From 1862cdf52efb397e3b3f374cf8407440de5972ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Jun 2021 18:06:33 +0200 Subject: [PATCH 001/818] Bump version to 2021.8.0dev0 (#52346) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 250a50bfada..62f0e92738e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Final MAJOR_VERSION: Final = 2021 -MINOR_VERSION: Final = 7 +MINOR_VERSION: Final = 8 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" From 63c727ac6cb5aec1446a30566ddf6096e0c99b27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jun 2021 11:09:19 -0500 Subject: [PATCH 002/818] Update homekit_controller to use async zeroconf (#52330) --- homeassistant/components/homekit_controller/__init__.py | 6 ++++-- homeassistant/components/homekit_controller/config_flow.py | 6 ++++-- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 44d8286984c..f7507d09837 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -219,8 +219,10 @@ async def async_setup(hass, config): map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() - zeroconf_instance = await zeroconf.async_get_instance(hass) - hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) + async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + hass.data[CONTROLLER] = aiohomekit.Controller( + async_zeroconf_instance=async_zeroconf_instance + ) hass.data[KNOWN_DEVICES] = {} hass.data[TRIGGERS] = {} diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 6ae66d362c9..ebb14e43378 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -99,8 +99,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_setup_controller(self): """Create the controller.""" - zeroconf_instance = await zeroconf.async_get_instance(self.hass) - self.controller = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) + async_zeroconf_instance = await zeroconf.async_get_async_instance(self.hass) + self.controller = aiohomekit.Controller( + async_zeroconf_instance=async_zeroconf_instance + ) async def async_step_user(self, user_input=None): """Handle a flow start.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7ff32e402fe..155f3a4f5f6 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.67"], + "requirements": ["aiohomekit==0.4.0"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index a0e10cdf991..13fc6ace1d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.67 +aiohomekit==0.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70585ece828..8e76f627281 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.67 +aiohomekit==0.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 4a94ed8f5a2b99da7b771bf740e5dc67349fc8b1 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Wed, 30 Jun 2021 22:24:19 +0300 Subject: [PATCH 003/818] Use attrs instead of property for Jewish Calendar (#52333) * Use attrs instead of property for Jewish Calendar * clean time sensor class * move type and prefix up * revert is_on attribute --- .../jewish_calendar/binary_sensor.py | 30 ++++-------------- .../components/jewish_calendar/sensor.py | 31 +++++-------------- 2 files changed, 13 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index bda2bd5a117..954b22debd0 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -29,41 +29,23 @@ class JewishCalendarBinarySensor(BinarySensorEntity): def __init__(self, data, sensor, sensor_info): """Initialize the binary sensor.""" - self._location = data["location"] self._type = sensor - self._name = f"{data['name']} {sensor_info[0]}" - self._icon = sensor_info[1] + self._prefix = data["prefix"] + self._attr_name = f"{data['name']} {sensor_info[0]}" + self._attr_unique_id = f"{self._prefix}_{self._type}" + self._attr_icon = sensor_info[1] + self._attr_should_poll = False + self._location = data["location"] self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] - self._prefix = data["prefix"] self._update_unsub = None - @property - def icon(self): - """Return the icon of the entity.""" - return self._icon - - @property - def unique_id(self) -> str: - """Generate a unique id.""" - return f"{self._prefix}_{self._type}" - - @property - def name(self): - """Return the name of the entity.""" - return self._name - @property def is_on(self): """Return true if sensor is on.""" return self._get_zmanim().issur_melacha_in_effect - @property - def should_poll(self): - """No polling needed.""" - return False - def _get_zmanim(self): """Return the Zmanim object for now().""" return hdate.Zmanim( diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 5690cd35a03..0dfc61970ef 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -35,33 +35,19 @@ class JewishCalendarSensor(SensorEntity): def __init__(self, data, sensor, sensor_info): """Initialize the Jewish calendar sensor.""" - self._location = data["location"] self._type = sensor - self._name = f"{data['name']} {sensor_info[0]}" - self._icon = sensor_info[1] + self._prefix = data["prefix"] + self._attr_name = f"{data['name']} {sensor_info[0]}" + self._attr_unique_id = f"{self._prefix}_{self._type}" + self._attr_icon = sensor_info[1] + self._location = data["location"] self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] self._state = None - self._prefix = data["prefix"] self._holiday_attrs = {} - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Generate a unique id.""" - return f"{self._prefix}_{self._type}" - - @property - def icon(self): - """Icon to display in the front end.""" - return self._icon - @property def state(self): """Return the state of the sensor.""" @@ -142,16 +128,13 @@ class JewishCalendarSensor(SensorEntity): class JewishCalendarTimeSensor(JewishCalendarSensor): """Implement attrbutes for sensors returning times.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + @property def state(self): """Return the state of the sensor.""" return dt_util.as_utc(self._state) if self._state is not None else None - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP - @property def extra_state_attributes(self): """Return the state attributes.""" From 635cf59909c370522e797f8b2db152fec36bbd8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Jun 2021 22:28:57 +0200 Subject: [PATCH 004/818] Remove deprecated YAML configuration from Abode (#52357) --- homeassistant/components/abode/__init__.py | 35 +----------------- homeassistant/components/abode/config_flow.py | 10 ------ tests/components/abode/test_config_flow.py | 36 +------------------ 3 files changed, 2 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 156dbae2804..4e7709f9bf7 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,5 +1,4 @@ """Support for the Abode Security System.""" -from copy import deepcopy from functools import partial from abodepy import Abode @@ -8,7 +7,6 @@ import abodepy.helpers.timeline as TIMELINE from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, @@ -44,22 +42,7 @@ ATTR_APP_TYPE = "app_type" ATTR_EVENT_BY = "event_by" ATTR_VALUE = "value" -CONFIG_SCHEMA = vol.Schema( - vol.All( - # Deprecated in Home Assistant 2021.6 - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_POLLING, default=False): cv.boolean, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) CHANGE_SETTING_SCHEMA = vol.Schema( {vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string} @@ -92,22 +75,6 @@ class AbodeSystem: self.logout_listener = None -async def async_setup(hass, config): - """Set up Abode integration.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf) - ) - ) - - return True - - async def async_setup_entry(hass, config_entry): """Set up Abode integration from a config entry.""" username = config_entry.data.get(CONF_USERNAME) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index bf51ffee81c..8b2f622d6e7 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -158,13 +158,3 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._password = user_input[CONF_PASSWORD] return await self._async_abode_login(step_id="reauth_confirm") - - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - if self._async_current_entries(): - LOGGER.warning("Already configured; Only a single configuration possible") - return self.async_abort(reason="single_instance_allowed") - - self._polling = import_config.get(CONF_POLLING, False) - - return await self.async_step_user(import_config) diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 806038194bb..b56762bff40 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -7,7 +7,7 @@ from abodepy.helpers.errors import MFA_CODE_REQUIRED from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.components.abode.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, @@ -46,17 +46,6 @@ async def test_one_config_allowed(hass): assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert step_user_result["reason"] == "single_instance_allowed" - conf = { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_POLLING: False, - } - - import_config_result = await flow.async_step_import(conf) - - assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert import_config_result["reason"] == "single_instance_allowed" - async def test_invalid_credentials(hass): """Test that invalid credentials throws an error.""" @@ -90,29 +79,6 @@ async def test_connection_error(hass): assert result["errors"] == {"base": "cannot_connect"} -async def test_step_import(hass): - """Test that the import step works.""" - conf = { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_POLLING: False, - } - - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "user@email.com" - assert result["data"] == { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_POLLING: False, - } - - async def test_step_user(hass): """Test that the user step works.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} From 04e631ed1f84d1b28c6991befd137b950b810662 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Jun 2021 22:29:44 +0200 Subject: [PATCH 005/818] Remove deprecated YAML configuration from VeSync (#52358) --- homeassistant/components/vesync/__init__.py | 39 +------------------ .../components/vesync/config_flow.py | 4 -- tests/components/vesync/test_config_flow.py | 12 ------ 3 files changed, 1 insertion(+), 54 deletions(-) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 3a17af55c93..48a7a577313 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -2,9 +2,7 @@ import logging from pyvesync import VeSync -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -25,42 +23,7 @@ PLATFORMS = ["switch", "fan", "light"] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the VeSync component.""" - conf = config.get(DOMAIN) - - if conf is None: - return True - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: conf[CONF_USERNAME], - CONF_PASSWORD: conf[CONF_PASSWORD], - }, - ) - ) - - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index f91848c5238..d9cd1bfc648 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -33,10 +33,6 @@ class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - async def async_step_import(self, import_config): - """Handle external yaml configuration.""" - return await self.async_step_user(import_config) - async def async_step_user(self, user_input=None): """Handle a flow start.""" if self._async_current_entries(): diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index f302d0ca5b3..cd68a0b5877 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -33,18 +33,6 @@ async def test_invalid_login_error(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_config_flow_configuration_yaml(hass): - """Test config flow with configuration.yaml user input.""" - test_dict = {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} - flow = config_flow.VeSyncFlowHandler() - flow.hass = hass - with patch("pyvesync.vesync.VeSync.login", return_value=True): - result = await flow.async_step_import(test_dict) - - assert result["data"].get(CONF_USERNAME) == test_dict[CONF_USERNAME] - assert result["data"].get(CONF_PASSWORD) == test_dict[CONF_PASSWORD] - - async def test_config_flow_user_input(hass): """Test config flow with user input.""" flow = config_flow.VeSyncFlowHandler() From b4e550dee2b07b0da614f3d937a471fb537bf935 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 1 Jul 2021 00:19:22 +0200 Subject: [PATCH 006/818] Bump pyatmo to v5.2.0 (#52365) * Bump pyatmo to v5.2.0 * Revert formatting changes --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index a6630a00f50..de7fbc36038 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==5.1.0" + "pyatmo==5.2.0" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 13fc6ace1d2..a7f3c781469 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1319,7 +1319,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.1.0 +pyatmo==5.2.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e76f627281..6ca5677143e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -747,7 +747,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.1.0 +pyatmo==5.2.0 # homeassistant.components.apple_tv pyatv==0.7.7 From cb2b6f5ff4950b758faf19341d0cd23ce750dfbe Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Jun 2021 17:30:54 -0500 Subject: [PATCH 007/818] Remove redundant property definitions in Ambient PWS (#52350) --- .../components/ambient_station/__init__.py | 78 ++++++------------- .../ambient_station/binary_sensor.py | 25 +++--- .../components/ambient_station/sensor.py | 24 ++---- 3 files changed, 42 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9036a4d89a2..347ba700060 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -493,62 +493,17 @@ class AmbientWeatherEntity(Entity): ): """Initialize the sensor.""" self._ambient = ambient - self._device_class = device_class - self._mac_address = mac_address - self._sensor_name = sensor_name - self._sensor_type = sensor_type - self._state = None - self._station_name = station_name - - @property - def available(self): - """Return True if entity is available.""" - # Since the solarradiation_lx sensor is created only if the - # user shows a solarradiation sensor, ensure that the - # solarradiation_lx sensor shows as available if the solarradiation - # sensor is available: - if self._sensor_type == TYPE_SOLARRADIATION_LX: - return ( - self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( - TYPE_SOLARRADIATION - ) - is not None - ) - return ( - self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( - self._sensor_type - ) - is not None - ) - - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - @property - def device_info(self): - """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._mac_address)}, - "name": self._station_name, + self._attr_device_class = device_class + self._attr_device_info = { + "identifiers": {(DOMAIN, mac_address)}, + "name": station_name, "manufacturer": "Ambient Weather", } - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._station_name}_{self._sensor_name}" - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def unique_id(self): - """Return a unique, unchanging string that represents this sensor.""" - return f"{self._mac_address}_{self._sensor_type}" + self._attr_name = f"{station_name}_{sensor_name}" + self._attr_should_poll = False + self._attr_unique_id = f"{mac_address}_{sensor_type}" + self._mac_address = mac_address + self._sensor_type = sensor_type async def async_added_to_hass(self): """Register callbacks.""" @@ -556,6 +511,21 @@ class AmbientWeatherEntity(Entity): @callback def update(): """Update the state.""" + if self._sensor_type == TYPE_SOLARRADIATION_LX: + self._attr_available = ( + self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + TYPE_SOLARRADIATION + ) + is not None + ) + else: + self._attr_available = ( + self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) + is not None + ) + self.update_from_latest_data() self.async_write_ha_state() diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index c2e5ad8b4f4..872b2f4b9fd 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -47,15 +47,19 @@ async def async_setup_entry(hass, entry, async_add_entities): ) ) - async_add_entities(binary_sensor_list, True) + async_add_entities(binary_sensor_list) class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): """Define an Ambient binary sensor.""" - @property - def is_on(self): - """Return the status of the sensor.""" + @callback + def update_from_latest_data(self): + """Fetch new state data for the entity.""" + state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) + if self._sensor_type in ( TYPE_BATT1, TYPE_BATT10, @@ -72,13 +76,6 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): TYPE_PM25_BATT, TYPE_PM25IN_BATT, ): - return self._state == 0 - - return self._state == 1 - - @callback - def update_from_latest_data(self): - """Fetch new state data for the entity.""" - self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( - self._sensor_type - ) + self._attr_is_on = state == 0 + else: + self._attr_is_on = state == 1 diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 7c60d1da9bc..69d41c035d0 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) ) - async_add_entities(sensor_list, True) + async_add_entities(sensor_list) class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): @@ -54,17 +54,7 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): ambient, mac_address, station_name, sensor_type, sensor_name, device_class ) - self._unit = unit - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit + self._attr_unit_of_measurement = unit @callback def update_from_latest_data(self): @@ -78,10 +68,10 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): ].get(TYPE_SOLARRADIATION) if w_m2_brightness_val is None: - self._state = None + self._attr_state = None else: - self._state = round(float(w_m2_brightness_val) / 0.0079) + self._attr_state = round(float(w_m2_brightness_val) / 0.0079) else: - self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( - self._sensor_type - ) + self._attr_state = self._ambient.stations[self._mac_address][ + ATTR_LAST_DATA + ].get(self._sensor_type) From 86f46753c94cdcae7eda39434b1528630ae9fd7e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 1 Jul 2021 00:12:17 +0000 Subject: [PATCH 008/818] [ci skip] Translation update --- .../cloudflare/translations/ca.json | 7 +++ .../cloudflare/translations/pl.json | 7 +++ .../components/coinbase/translations/ca.json | 40 +++++++++++++++ .../components/coinbase/translations/de.json | 2 +- .../components/coinbase/translations/no.json | 2 +- .../components/coinbase/translations/pl.json | 40 +++++++++++++++ .../coinbase/translations/zh-Hant.json | 2 +- .../devolo_home_control/translations/ca.json | 6 ++- .../devolo_home_control/translations/pl.json | 6 ++- .../components/dsmr/translations/pl.json | 36 ++++++++++++- .../forecast_solar/translations/ca.json | 31 ++++++++++++ .../forecast_solar/translations/pl.json | 31 ++++++++++++ .../freedompro/translations/ca.json | 20 ++++++++ .../freedompro/translations/de.json | 20 ++++++++ .../freedompro/translations/en.json | 2 +- .../freedompro/translations/et.json | 20 ++++++++ .../freedompro/translations/pl.json | 20 ++++++++ .../freedompro/translations/ru.json | 20 ++++++++ .../freedompro/translations/zh-Hant.json | 20 ++++++++ .../meteoclimatic/translations/es.json | 2 +- .../meteoclimatic/translations/gl.json | 9 ++++ .../meteoclimatic/translations/nl.json | 2 +- .../meteoclimatic/translations/pt.json | 9 ++++ .../nmap_tracker/translations/ca.json | 38 ++++++++++++++ .../nmap_tracker/translations/de.json | 18 +++++++ .../nmap_tracker/translations/pl.json | 38 ++++++++++++++ .../components/onvif/translations/ca.json | 13 +++++ .../components/onvif/translations/pl.json | 13 +++++ .../philips_js/translations/ca.json | 9 ++++ .../philips_js/translations/pl.json | 9 ++++ .../components/wled/translations/pl.json | 9 ++++ .../components/zwave_js/translations/pl.json | 50 +++++++++++++++++++ 32 files changed, 539 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/ca.json create mode 100644 homeassistant/components/coinbase/translations/pl.json create mode 100644 homeassistant/components/forecast_solar/translations/ca.json create mode 100644 homeassistant/components/forecast_solar/translations/pl.json create mode 100644 homeassistant/components/freedompro/translations/ca.json create mode 100644 homeassistant/components/freedompro/translations/de.json create mode 100644 homeassistant/components/freedompro/translations/et.json create mode 100644 homeassistant/components/freedompro/translations/pl.json create mode 100644 homeassistant/components/freedompro/translations/ru.json create mode 100644 homeassistant/components/freedompro/translations/zh-Hant.json create mode 100644 homeassistant/components/meteoclimatic/translations/gl.json create mode 100644 homeassistant/components/meteoclimatic/translations/pt.json create mode 100644 homeassistant/components/nmap_tracker/translations/ca.json create mode 100644 homeassistant/components/nmap_tracker/translations/pl.json diff --git a/homeassistant/components/cloudflare/translations/ca.json b/homeassistant/components/cloudflare/translations/ca.json index d0ffdcd5429..df26eaa73bc 100644 --- a/homeassistant/components/cloudflare/translations/ca.json +++ b/homeassistant/components/cloudflare/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "unknown": "Error inesperat" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "Token d'API", + "description": "Torna a autenticar-te amb el compte de Cloudflare." + } + }, "records": { "data": { "records": "Registres" diff --git a/homeassistant/components/cloudflare/translations/pl.json b/homeassistant/components/cloudflare/translations/pl.json index 7675c7df8a3..94a87c34b2e 100644 --- a/homeassistant/components/cloudflare/translations/pl.json +++ b/homeassistant/components/cloudflare/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "Token API", + "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Cloudflare." + } + }, "records": { "data": { "records": "Rekordy" diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json new file mode 100644 index 00000000000..0b512498fcf --- /dev/null +++ b/homeassistant/components/coinbase/translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "api_token": "Secret API", + "currencies": "Monedes del saldo del compte", + "exchange_rates": "Tipus de canvi" + }, + "description": "Introdueix els detalls de la teva clau API tal com els proporciona Coinbase.", + "title": "Detalls de la clau API de Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "L'API de Coinbase no proporciona algun/s dels saldos de moneda que has sol\u00b7licitat.", + "exchange_rate_unavaliable": "L'API de Coinbase no proporciona algun/s dels tipus de canvi que has sol\u00b7licitat.", + "unknown": "Error inesperat" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Saldos de cartera a informar.", + "exchange_rate_currencies": "Tipus de canvi a informar." + }, + "description": "Ajusta les opcions de Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 37ccdedd81a..60195c794a1 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -16,7 +16,7 @@ "currencies": "Kontostand W\u00e4hrungen", "exchange_rates": "Wechselkurse" }, - "description": "Bitte gib die Details Ihres API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt. Trenne mehrere W\u00e4hrungen mit einem Komma (z. B. \"BTC, EUR\")", + "description": "Bitte gib die Details deines API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt. Trenne mehrere W\u00e4hrungen mit einem Komma (z. B. \"BTC, EUR\")", "title": "Coinbase API Schl\u00fcssel Details" } } diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index 265ea29e01c..747049fbd5c 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -16,7 +16,7 @@ "currencies": "Valutaer for kontosaldo", "exchange_rates": "Valutakurser" }, - "description": "Vennligst skriv inn detaljene for API-n\u00f8kkelen din som gitt av Coinbase. Skill flere valutaer med komma (f.eks. \"BTC, EUR\")", + "description": "Vennligst skriv inn detaljene for API-n\u00f8kkelen din som gitt av Coinbase.", "title": "Detaljer for Coinbase API-n\u00f8kkel" } } diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json new file mode 100644 index 00000000000..a5918556892 --- /dev/null +++ b/homeassistant/components/coinbase/translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "api_token": "Sekretne API", + "currencies": "Waluty salda konta", + "exchange_rates": "Kursy wymiany" + }, + "description": "Wprowad\u017a dane swojego klucza API podane przez Coinbase.", + "title": "Szczeg\u00f3\u0142y klucza API Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.", + "exchange_rate_unavaliable": "Jeden lub wi\u0119cej z \u017c\u0105danych kurs\u00f3w wymiany nie jest dostarczany przez Coinbase.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Salda portfela do zg\u0142oszenia.", + "exchange_rate_currencies": "Kursy walut do zg\u0142oszenia." + }, + "description": "Dostosuj opcje Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index aa00e459591..1ea37aa68ad 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -16,7 +16,7 @@ "currencies": "\u5e33\u6236\u9918\u984d\u8ca8\u5e63", "exchange_rates": "\u532f\u7387" }, - "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u9470\u8cc7\u8a0a\u3002\u4ee5\u9017\u865f\u5206\u9694\u591a\u7a2e\u8ca8\u5e63\uff08\u4f8b\u5982 \"BTC, EUR\"\uff09", + "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u9470\u8cc7\u8a0a\u3002", "title": "Coinbase API \u5bc6\u9470\u8cc7\u6599" } } diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json index a41c1f78c15..968624e15c8 100644 --- a/homeassistant/components/devolo_home_control/translations/ca.json +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "reauth_failed": "Utilitza el mateix usuari de mydevolo que abans." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/pl.json b/homeassistant/components/devolo_home_control/translations/pl.json index 388ea692ad4..0c0f18b1d6a 100644 --- a/homeassistant/components/devolo_home_control/translations/pl.json +++ b/homeassistant/components/devolo_home_control/translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie", + "reauth_failed": "U\u017cyj tego samego u\u017cytkownika mydevolo co poprzednio." }, "step": { "user": { diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json index e8b8bf617f0..84a04cff625 100644 --- a/homeassistant/components/dsmr/translations/pl.json +++ b/homeassistant/components/dsmr/translations/pl.json @@ -1,9 +1,14 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_communicate": "Nie uda\u0142o si\u0119 nawi\u0105za\u0107 \u0142\u0105czno\u015bci", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_communicate": "Nie uda\u0142o si\u0119 nawi\u0105za\u0107 \u0142\u0105czno\u015bci", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "few": "kilka", "many": "wiele", "one": "jeden", @@ -13,7 +18,34 @@ "few": "kilka", "many": "wiele", "one": "jeden", - "other": "inne" + "other": "inne", + "setup_network": { + "data": { + "dsmr_version": "Wybierz wersj\u0119 DSMR", + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "title": "Wybierz adres dla po\u0142\u0105czenia" + }, + "setup_serial": { + "data": { + "dsmr_version": "Wybierz wersj\u0119 DSMR", + "port": "Wybierz urz\u0105dzenie" + }, + "title": "Urz\u0105dzenie" + }, + "setup_serial_manual_path": { + "data": { + "port": "\u015acie\u017cka urz\u0105dzenia USB" + }, + "title": "\u015acie\u017cka" + }, + "user": { + "data": { + "type": "Rodzaj po\u0142\u0105czenia" + }, + "title": "Wybierz typ po\u0142\u0105czenia" + } } }, "options": { diff --git a/homeassistant/components/forecast_solar/translations/ca.json b/homeassistant/components/forecast_solar/translations/ca.json new file mode 100644 index 00000000000..66e29caedb1 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimut (360 graus, 0 = nord, 90 = est, 180 = sud, 270 = oest)", + "declination": "Inclinaci\u00f3 (0 = horitzontal, 90 = vertical)", + "latitude": "Latitud", + "longitude": "Longitud", + "modules power": "Pot\u00e8ncia m\u00e0xima total dels panells solars", + "name": "Nom" + }, + "description": "Introdueix les dades dels teus panells solars. Consulta la documentaci\u00f3 si tens dubtes en algun camp." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Clau API de Forecast.Solar (opcional)", + "azimuth": "Azimut (360 graus, 0 = nord, 90 = est, 180 = sud, 270 = oest)", + "damping": "Factor d'amortiment: ajusta els resultats al mat\u00ed i al vespre", + "declination": "Inclinaci\u00f3 (0 = horitzontal, 90 = vertical)", + "modules power": "Pot\u00e8ncia m\u00e0xima total dels panells solars" + }, + "description": "Aquests valors permeten ajustar els resultats de Solar.Forecast. Consulta la documentaci\u00f3 si tens dubtes en algun camp." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/pl.json b/homeassistant/components/forecast_solar/translations/pl.json new file mode 100644 index 00000000000..8c1f96ea709 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azymut (360 stopni, 0 = P\u00f3\u0142noc, 90 = Wsch\u00f3d, 180 = Po\u0142udnie, 270 = Zach\u00f3d)", + "declination": "Deklinacja (0 = Poziomo, 90 = Pionowo)", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "modules power": "Ca\u0142kowita moc szczytowa modu\u0142\u00f3w fotowoltaicznych w watach", + "name": "Nazwa" + }, + "description": "Wpisz dane swoich paneli s\u0142onecznych. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Klucz API dla Forecast.Solar (opcjonalnie)", + "azimuth": "Azymut (360 stopni, 0 = P\u00f3\u0142noc, 90 = Wsch\u00f3d, 180 = Po\u0142udnie, 270 = Zach\u00f3d)", + "damping": "Wsp\u00f3\u0142czynnik t\u0142umienia: dostosowuje wyniki rano i wieczorem", + "declination": "Deklinacja (0 = Poziomo, 90 = Pionowo)", + "modules power": "Ca\u0142kowita moc szczytowa modu\u0142\u00f3w fotowoltaicznych w watach" + }, + "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Solar.Forecast. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/ca.json b/homeassistant/components/freedompro/translations/ca.json new file mode 100644 index 00000000000..29fc97d35ff --- /dev/null +++ b/homeassistant/components/freedompro/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix la clau API obtinguda de https://home.freedompro.eu", + "title": "Clau API de Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/de.json b/homeassistant/components/freedompro/translations/de.json new file mode 100644 index 00000000000..7ac985baeee --- /dev/null +++ b/homeassistant/components/freedompro/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Bitte gib den API-Schl\u00fcssel ein, den du von https://home.freedompro.eu erhalten hast.", + "title": "Freedompro API-Schl\u00fcssel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/en.json b/homeassistant/components/freedompro/translations/en.json index c8952d56bfd..83c36c43b64 100644 --- a/homeassistant/components/freedompro/translations/en.json +++ b/homeassistant/components/freedompro/translations/en.json @@ -17,4 +17,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/et.json b/homeassistant/components/freedompro/translations/et.json new file mode 100644 index 00000000000..16e5f414264 --- /dev/null +++ b/homeassistant/components/freedompro/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Sisesta aadressilt https://home.freedompro.eu saadud API v\u00f5ti", + "title": "Freedompro API v\u00f5ti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/pl.json b/homeassistant/components/freedompro/translations/pl.json new file mode 100644 index 00000000000..62985add95a --- /dev/null +++ b/homeassistant/components/freedompro/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API" + }, + "description": "Wprowad\u017a klucz API uzyskany z https://home.freedompro.eu", + "title": "Klucz API Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/ru.json b/homeassistant/components/freedompro/translations/ru.json new file mode 100644 index 00000000000..db1523bfecb --- /dev/null +++ b/homeassistant/components/freedompro/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 https://home.freedompro.eu", + "title": "\u041a\u043b\u044e\u0447 API Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/zh-Hant.json b/homeassistant/components/freedompro/translations/zh-Hant.json new file mode 100644 index 00000000000..2baa8719e2e --- /dev/null +++ b/homeassistant/components/freedompro/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165\u7531 https://home.freedompro.eu \u6240\u7372\u5f97\u7684 API \u5bc6\u9470", + "title": "Freedompro API \u5bc6\u9470" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/es.json b/homeassistant/components/meteoclimatic/translations/es.json index 251fcbe8e09..fd9a38db804 100644 --- a/homeassistant/components/meteoclimatic/translations/es.json +++ b/homeassistant/components/meteoclimatic/translations/es.json @@ -5,7 +5,7 @@ "data": { "code": "C\u00f3digo de la estaci\u00f3n" }, - "description": "Introduzca el c\u00f3digo de la estaci\u00f3n Meteoclimatic (por ejemplo, ESCAT430000000043206B)", + "description": "Introduzca el c\u00f3digo de la estaci\u00f3n Meteoclimatic (por ejemplo, ESCAT4300000043206B)", "title": "Meteoclimatic" } } diff --git a/homeassistant/components/meteoclimatic/translations/gl.json b/homeassistant/components/meteoclimatic/translations/gl.json new file mode 100644 index 00000000000..be1629dd6e4 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/gl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Introduza o c\u00f3digo da estaci\u00f3n Meteoclimatic (por exemplo, ESCAT4300000043206B)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/nl.json b/homeassistant/components/meteoclimatic/translations/nl.json index 0b4aa397276..dd2a318ec37 100644 --- a/homeassistant/components/meteoclimatic/translations/nl.json +++ b/homeassistant/components/meteoclimatic/translations/nl.json @@ -12,7 +12,7 @@ "data": { "code": "Station code" }, - "description": "Voer de code van het meteoklimatologische station in (bv. ESCAT43000043206B)", + "description": "Voer de code van het meteoklimatologische station in (bv. ESCAT4300000043206B)", "title": "Meteoclimatic" } } diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json new file mode 100644 index 00000000000..71eded9f5de --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Introduza o c\u00f3digo da esta\u00e7\u00e3o Meteoclimatic (por exemplo, ESCAT4300000043206B)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ca.json b/homeassistant/components/nmap_tracker/translations/ca.json new file mode 100644 index 00000000000..0912179fafd --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "invalid_hosts": "Amfitrions no v\u00e0lids" + }, + "step": { + "user": { + "data": { + "exclude": "Adreces de xarxa a excloure de l'escaneig (separades per comes)", + "home_interval": "Nombre m\u00ednim de minuts entre escanejos de dispositius actius (conserva la bateria)", + "hosts": "Adreces de xarxa a escanejar (separades per comes)", + "scan_options": "Opcions de configuraci\u00f3 d'escaneig d'Nmap en brut" + }, + "description": "Configura els amfitrions a explorar per Nmap. L'adre\u00e7a de xarxa i les exclusions poden ser adreces IP (192.168.1.1), xarxes IP (192.168.0.0/24) o intervals IP (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Amfitrions no v\u00e0lids" + }, + "step": { + "init": { + "data": { + "exclude": "Adreces de xarxa a excloure de l'escaneig (separades per comes)", + "home_interval": "Nombre m\u00ednim de minuts entre escanejos de dispositius actius (conserva la bateria)", + "hosts": "Adreces de xarxa a escanejar (separades per comes)", + "scan_options": "Opcions de configuraci\u00f3 d'escaneig d'Nmap en brut" + }, + "description": "Configura els amfitrions a explorar per Nmap. L'adre\u00e7a de xarxa i les exclusions poden ser adreces IP (192.168.1.1), xarxes IP (192.168.0.0/24) o intervals IP (192.168.1.0-32)." + } + } + }, + "title": "Seguidor Nmap" +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json index 67c7e8dbd8a..351c09036fe 100644 --- a/homeassistant/components/nmap_tracker/translations/de.json +++ b/homeassistant/components/nmap_tracker/translations/de.json @@ -2,12 +2,30 @@ "config": { "abort": { "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "invalid_hosts": "Ung\u00fcltige Hosts" + }, + "step": { + "user": { + "data": { + "exclude": "Netzwerkadressen (kommagetrennt), die von der \u00dcberpr\u00fcfung ausgeschlossen werden sollen", + "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", + "hosts": "Netzwerkadressen (kommagetrennt) zum Scannen", + "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap" + }, + "description": "Konfiguriere die Hosts, die von Nmap gescannt werden sollen. Netzwerkadresse und Ausschl\u00fcsse k\u00f6nnen IP-Adressen (192.168.1.1), IP-Netzwerke (192.168.0.0/24) oder IP-Bereiche (192.168.1.0-32) sein." + } } }, "options": { + "error": { + "invalid_hosts": "Ung\u00fcltige Hosts" + }, "step": { "init": { "data": { + "exclude": "Netzwerkadressen (kommagetrennt), die von der \u00dcberpr\u00fcfung ausgeschlossen werden sollen", "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", "hosts": "Zu scannende Netzwerkadressen (kommagetrennt)", "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap" diff --git a/homeassistant/components/nmap_tracker/translations/pl.json b/homeassistant/components/nmap_tracker/translations/pl.json new file mode 100644 index 00000000000..c0fbd9a3a70 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "invalid_hosts": "Nieprawid\u0142owe hosta" + }, + "step": { + "user": { + "data": { + "exclude": "Adresy sieciowe (rozdzielone przecinkami) do wykluczenia ze skanowania", + "home_interval": "Minimalna liczba minut mi\u0119dzy skanami aktywnych urz\u0105dze\u0144 (oszcz\u0119dzanie baterii)", + "hosts": "Adresy sieciowe (oddzielone przecinkami) do skanowania", + "scan_options": "Surowe konfigurowalne opcje skanowania dla Nmap" + }, + "description": "Skonfiguruj hosta do skanowania przez Nmap. Adresy sieciowe i te wykluczone mog\u0105 by\u0107 adresami IP (192.168.1.1), sieciami IP (192.168.0.0/24) lub zakresami IP (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Nieprawid\u0142owe hosta" + }, + "step": { + "init": { + "data": { + "exclude": "Adresy sieciowe (rozdzielone przecinkami) do wykluczenia ze skanowania", + "home_interval": "Minimalna liczba minut mi\u0119dzy skanami aktywnych urz\u0105dze\u0144 (oszcz\u0119dzanie baterii)", + "hosts": "Adresy sieciowe (oddzielone przecinkami) do skanowania", + "scan_options": "Surowe konfigurowalne opcje skanowania dla Nmap" + }, + "description": "Skonfiguruj hosta do skanowania przez Nmap. Adresy sieciowe i te wykluczone mog\u0105 by\u0107 adresami IP (192.168.1.1), sieciami IP (192.168.0.0/24) lub zakresami IP (192.168.1.0-32)." + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ca.json b/homeassistant/components/onvif/translations/ca.json index a364552f9b7..7ede84d4845 100644 --- a/homeassistant/components/onvif/translations/ca.json +++ b/homeassistant/components/onvif/translations/ca.json @@ -18,6 +18,16 @@ }, "title": "Configuraci\u00f3 d'autenticaci\u00f3" }, + "configure": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 de dispositiu ONVIF" + }, "configure_profile": { "data": { "include": "Crea entitat de c\u00e0mera" @@ -40,6 +50,9 @@ "title": "Configura el dispositiu ONVIF" }, "user": { + "data": { + "auto": "Cerca autom\u00e0ticament" + }, "description": "En fer clic a envia, es cercaran a la xarxa dispositius ONVIF que suportin perfils S.\n\nAlguns fabricants han comen\u00e7at a desactivar ONVIF per defecte. Comprova que ONVIF est\u00e0 activat a la configuraci\u00f3 de les c\u00e0meres.", "title": "Configuraci\u00f3 de dispositiu ONVIF" } diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json index 2d2f55728ad..0b72b824abf 100644 --- a/homeassistant/components/onvif/translations/pl.json +++ b/homeassistant/components/onvif/translations/pl.json @@ -18,6 +18,16 @@ }, "title": "Konfiguracja uwierzytelniania" }, + "configure": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja urz\u0105dzenia ONVIF" + }, "configure_profile": { "data": { "include": "Utw\u00f3rz encj\u0119 kamery" @@ -40,6 +50,9 @@ "title": "Konfiguracja urz\u0105dzenia ONVIF" }, "user": { + "data": { + "auto": "Wyszukaj automatycznie" + }, "description": "Klikaj\u0105c przycisk Zatwierd\u017a, Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", "title": "Konfiguracja urz\u0105dzenia ONVIF" } diff --git a/homeassistant/components/philips_js/translations/ca.json b/homeassistant/components/philips_js/translations/ca.json index b94faccd615..1999639623c 100644 --- a/homeassistant/components/philips_js/translations/ca.json +++ b/homeassistant/components/philips_js/translations/ca.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Es demani que el dispositiu s'engegui" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Permet l'\u00fas del servei de notificaci\u00f3 de dades." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/pl.json b/homeassistant/components/philips_js/translations/pl.json index a89b6136ff8..fce4ac34c83 100644 --- a/homeassistant/components/philips_js/translations/pl.json +++ b/homeassistant/components/philips_js/translations/pl.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Urz\u0105dzenie zostanie poproszone o w\u0142\u0105czenie" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Zezw\u00f3l na korzystanie z us\u0142ugi powiadamiania o danych." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index 423c30d1fe8..c4a2efc43a1 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -20,5 +20,14 @@ "title": "Wykryto urz\u0105dzenie WLED" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Utw\u00f3rz encj\u0119 \"master light\", nawet z 1 segmentem LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index b729e8db3da..c8b7b8b0ed9 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", + "addon_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji o dodatku Z-Wave JS", + "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS", + "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS", + "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "different_device": "Pod\u0142\u0105czone urz\u0105dzenie USB nie jest takie samo, jak wcze\u015bniej skonfigurowane dla tego wpisu konfiguracyjnego. Zamiast tego, utw\u00f3rz nowy wpis konfiguracyjny dla nowego urz\u0105dzenia." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_ws_url": "Nieprawid\u0142owy URL websocket", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "progress": { + "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut.", + "start_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 uruchamianie dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 chwil\u0119." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulacja sprz\u0119tu", + "log_level": "Poziom loga", + "network_key": "Klucz sieci", + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + }, + "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" + }, + "install_addon": { + "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "U\u017cyj dodatku Z-Wave JS Supervisor" + }, + "description": "Czy chcesz skorzysta\u0107 z dodatku Z-Wave JS Supervisor?", + "title": "Wybierz metod\u0119 po\u0142\u0105czenia" + }, + "start_addon": { + "title": "Dodatek Z-Wave JS uruchamia si\u0119..." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file From f98a5cc2334912609d6080a5ce5fa0d8322b8c7a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Jul 2021 00:31:46 -0500 Subject: [PATCH 009/818] Remove redundant property definitions in IQVIA (#52378) --- homeassistant/components/iqvia/__init__.py | 40 ++++------------------ homeassistant/components/iqvia/sensor.py | 24 ++++++++----- 2 files changed, 21 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 5d13a2373a6..30a54fa1fd0 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -104,43 +104,15 @@ class IQVIAEntity(CoordinatorEntity, SensorEntity): def __init__(self, coordinator, entry, sensor_type, name, icon): """Initialize.""" super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_icon = icon + self._attr_name = name + self._attr_unique_id = f"{entry.data[CONF_ZIP_CODE]}_{sensor_type}" + self._attr_unit_of_measurement = "index" self._entry = entry - self._icon = icon - self._name = name - self._state = None self._type = sensor_type - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._entry.data[CONF_ZIP_CODE]}_{self._type}" - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return "index" - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index b0420a52ee9..0ff236a8f79 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -120,7 +120,7 @@ class ForecastSensor(IQVIAEntity): if i["minimum"] <= average <= i["maximum"] ] - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_CITY: data["City"].title(), ATTR_RATING: rating, @@ -134,10 +134,14 @@ class ForecastSensor(IQVIAEntity): outlook_coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR][ self._entry.entry_id ][TYPE_ALLERGY_OUTLOOK] - self._attrs[ATTR_OUTLOOK] = outlook_coordinator.data.get("Outlook") - self._attrs[ATTR_SEASON] = outlook_coordinator.data.get("Season") + self._attr_extra_state_attributes[ + ATTR_OUTLOOK + ] = outlook_coordinator.data.get("Outlook") + self._attr_extra_state_attributes[ + ATTR_SEASON + ] = outlook_coordinator.data.get("Season") - self._state = average + self._attr_state = average class IndexSensor(IQVIAEntity): @@ -172,7 +176,7 @@ class IndexSensor(IQVIAEntity): if i["minimum"] <= period["Index"] <= i["maximum"] ] - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_CITY: data["City"].title(), ATTR_RATING: rating, @@ -184,7 +188,7 @@ class IndexSensor(IQVIAEntity): if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): for idx, attrs in enumerate(period["Triggers"]): index = idx + 1 - self._attrs.update( + self._attr_extra_state_attributes.update( { f"{ATTR_ALLERGEN_GENUS}_{index}": attrs["Genus"], f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"], @@ -194,7 +198,7 @@ class IndexSensor(IQVIAEntity): elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): for idx, attrs in enumerate(period["Triggers"]): index = idx + 1 - self._attrs.update( + self._attr_extra_state_attributes.update( { f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"], f"{ATTR_ALLERGEN_AMOUNT}_{index}": attrs["PPM"], @@ -202,6 +206,8 @@ class IndexSensor(IQVIAEntity): ) elif self._type == TYPE_DISEASE_TODAY: for attrs in period["Triggers"]: - self._attrs[f"{attrs['Name'].lower()}_index"] = attrs["Index"] + self._attr_extra_state_attributes[ + f"{attrs['Name'].lower()}_index" + ] = attrs["Index"] - self._state = period["Index"] + self._attr_state = period["Index"] From 72b55d183025ee38219b3dd2dbe337971e7aa167 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 1 Jul 2021 07:48:29 +0200 Subject: [PATCH 010/818] Bump bt_proximity (#52364) --- homeassistant/components/bluetooth_tracker/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index a41720c2c4f..ccf48a9b8c3 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -2,7 +2,7 @@ "domain": "bluetooth_tracker", "name": "Bluetooth Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", - "requirements": ["bt_proximity==0.2", "pybluez==0.22"], + "requirements": ["bt_proximity==0.2.1", "pybluez==0.22"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a7f3c781469..674956249fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ brunt==0.1.3 bsblan==0.4.0 # homeassistant.components.bluetooth_tracker -bt_proximity==0.2 +bt_proximity==0.2.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 From f8eb07444b098862986bbbec6886ba56de9e76f8 Mon Sep 17 00:00:00 2001 From: Bruce Sheplan Date: Thu, 1 Jul 2021 03:11:01 -0500 Subject: [PATCH 011/818] Add screenlogic reconnect (#52022) Co-authored-by: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> --- .../components/screenlogic/__init__.py | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 521a1ea798c..223ca9262ee 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -40,25 +40,8 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" - mac = entry.unique_id - # Attempt to re-discover named gateway to follow IP changes - discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS] - if mac in discovered_gateways: - connect_info = discovered_gateways[mac] - else: - _LOGGER.warning("Gateway rediscovery failed") - # Static connection defined or fallback from discovery - connect_info = { - SL_GATEWAY_NAME: name_for_mac(mac), - SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS], - SL_GATEWAY_PORT: entry.data[CONF_PORT], - } - try: - gateway = ScreenLogicGateway(**connect_info) - except ScreenLogicError as ex: - _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) - raise ConfigEntryNotReady from ex + gateway = await hass.async_add_executor_job(get_new_gateway, hass, entry) # The api library uses a shared socket connection and does not handle concurrent # requests very well. @@ -99,6 +82,39 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) +def get_connect_info(hass: HomeAssistant, entry: ConfigEntry): + """Construct connect_info from configuration entry and returns it to caller.""" + mac = entry.unique_id + # Attempt to re-discover named gateway to follow IP changes + discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS] + if mac in discovered_gateways: + connect_info = discovered_gateways[mac] + else: + _LOGGER.warning("Gateway rediscovery failed") + # Static connection defined or fallback from discovery + connect_info = { + SL_GATEWAY_NAME: name_for_mac(mac), + SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS], + SL_GATEWAY_PORT: entry.data[CONF_PORT], + } + + return connect_info + + +def get_new_gateway(hass: HomeAssistant, entry: ConfigEntry): + """Instantiate a new ScreenLogicGateway, connect to it and return it to caller.""" + + connect_info = get_connect_info(hass, entry) + + try: + gateway = ScreenLogicGateway(**connect_info) + except ScreenLogicError as ex: + _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) + raise ConfigEntryNotReady from ex + + return gateway + + class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage the data update for the Screenlogic component.""" @@ -119,13 +135,32 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): update_interval=interval, ) + def reconnect_gateway(self): + """Instantiate a new ScreenLogicGateway, connect to it and update. Return new gateway to caller.""" + + connect_info = get_connect_info(self.hass, self.config_entry) + + try: + gateway = ScreenLogicGateway(**connect_info) + gateway.update() + except ScreenLogicError as error: + raise UpdateFailed(error) from error + + return gateway + async def _async_update_data(self): """Fetch data from the Screenlogic gateway.""" try: async with self.api_lock: await self.hass.async_add_executor_job(self.gateway.update) except ScreenLogicError as error: - raise UpdateFailed(error) from error + _LOGGER.warning("ScreenLogicError - attempting reconnect: %s", error) + + async with self.api_lock: + self.gateway = await self.hass.async_add_executor_job( + self.reconnect_gateway + ) + return self.gateway.get_data() From 8f70b5c1833390c130541e558b78755743a46dd1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Jul 2021 03:35:14 -0500 Subject: [PATCH 012/818] Fix missing default latitude/longitude/elevation in OpenUV config flow (#52380) --- .../components/openuv/config_flow.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 2ed6b56d914..e31cef9ee0a 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -14,26 +14,35 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude, - vol.Optional(CONF_ELEVATION): vol.Coerce(float), - } -) - class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 + @property + def config_schema(self): + """Return the config schema.""" + return vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Inclusive( + CONF_LATITUDE, "coords", default=self.hass.config.latitude + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, "coords", default=self.hass.config.longitude + ): cv.longitude, + vol.Optional( + CONF_ELEVATION, default=self.hass.config.elevation + ): vol.Coerce(float), + } + ) + async def _show_form(self, errors=None): """Show the form to the user.""" return self.async_show_form( step_id="user", - data_schema=CONFIG_SCHEMA, + data_schema=self.config_schema, errors=errors if errors else {}, ) From 7bd8e2aa55c9a186f13a3ab103f4a0d96f06598b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Jul 2021 04:04:39 -0500 Subject: [PATCH 013/818] Remove redundant property definitions in Flu Near You (#52377) --- .../components/flunearyou/__init__.py | 7 +- homeassistant/components/flunearyou/sensor.py | 80 ++++++------------- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 6eb4d54fe4f..d9203ed0c29 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -25,14 +25,9 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["sensor"] -async def async_setup(hass, config): - """Set up the Flu Near You component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} - return True - - async def async_setup_entry(hass, entry): """Set up Flu Near You as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} websession = aiohttp_client.async_get_clientsession(hass) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 066126c390e..244d9120d7d 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -53,9 +53,9 @@ EXTENDED_SENSOR_TYPE_MAPPING = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up Flu Near You sensors based on a config entry.""" - coordinators = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] sensors = [] @@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors.append( CdcSensor( coordinators[CATEGORY_CDC_REPORT], - config_entry, + entry, sensor_type, name, icon, @@ -75,7 +75,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors.append( UserSensor( coordinators[CATEGORY_USER_REPORT], - config_entry, + entry, sensor_type, name, icon, @@ -89,49 +89,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" - def __init__(self, coordinator, config_entry, sensor_type, name, icon, unit): + def __init__(self, coordinator, entry, sensor_type, name, icon, unit): """Initialize the sensor.""" super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._config_entry = config_entry - self._icon = icon - self._name = name - self._sensor_type = sensor_type - self._state = None - self._unit = unit - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return ( - f"{self._config_entry.data[CONF_LATITUDE]}," - f"{self._config_entry.data[CONF_LONGITUDE]}_{self._sensor_type}" + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_icon = icon + self._attr_name = name + self._attr_unique_id = ( + f"{entry.data[CONF_LATITUDE]}," + f"{entry.data[CONF_LONGITUDE]}_{sensor_type}" ) - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit + self._attr_unit_of_measurement = unit + self._entry = entry + self._sensor_type = sensor_type @callback def _handle_coordinator_update(self) -> None: @@ -156,13 +126,13 @@ class CdcSensor(FluNearYouSensor): @callback def update_from_latest_data(self): """Update the sensor.""" - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_REPORTED_DATE: self.coordinator.data["week_date"], ATTR_STATE: self.coordinator.data["name"], } ) - self._state = self.coordinator.data[self._sensor_type] + self._attr_state = self.coordinator.data[self._sensor_type] class UserSensor(FluNearYouSensor): @@ -171,7 +141,7 @@ class UserSensor(FluNearYouSensor): @callback def update_from_latest_data(self): """Update the sensor.""" - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_CITY: self.coordinator.data["local"]["city"].split("(")[0], ATTR_REPORTED_LATITUDE: self.coordinator.data["local"]["latitude"], @@ -186,15 +156,15 @@ class UserSensor(FluNearYouSensor): elif self._sensor_type in EXTENDED_SENSOR_TYPE_MAPPING: states_key = EXTENDED_SENSOR_TYPE_MAPPING[self._sensor_type] - self._attrs[ATTR_STATE_REPORTS_THIS_WEEK] = self.coordinator.data["state"][ - "data" - ][states_key] - self._attrs[ATTR_STATE_REPORTS_LAST_WEEK] = self.coordinator.data["state"][ - "last_week_data" - ][states_key] + self._attr_extra_state_attributes[ + ATTR_STATE_REPORTS_THIS_WEEK + ] = self.coordinator.data["state"]["data"][states_key] + self._attr_extra_state_attributes[ + ATTR_STATE_REPORTS_LAST_WEEK + ] = self.coordinator.data["state"]["last_week_data"][states_key] if self._sensor_type == SENSOR_TYPE_USER_TOTAL: - self._state = sum( + self._attr_state = sum( v for k, v in self.coordinator.data["local"].items() if k @@ -207,4 +177,4 @@ class UserSensor(FluNearYouSensor): ) ) else: - self._state = self.coordinator.data["local"][self._sensor_type] + self._attr_state = self.coordinator.data["local"][self._sensor_type] From 2868fef7d4f2c3ca0aad757ce35db5ca841e1222 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 1 Jul 2021 02:22:43 -0700 Subject: [PATCH 014/818] Add motion detection support to motionEye (#49665) --- .../components/motioneye/__init__.py | 201 +++++++++- .../components/motioneye/config_flow.py | 57 ++- homeassistant/components/motioneye/const.py | 94 ++++- .../components/motioneye/manifest.json | 6 +- .../components/motioneye/strings.json | 10 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/motioneye/__init__.py | 13 +- tests/components/motioneye/test_camera.py | 8 +- .../components/motioneye/test_config_flow.py | 43 ++- tests/components/motioneye/test_web_hooks.py | 348 ++++++++++++++++++ 11 files changed, 751 insertions(+), 33 deletions(-) create mode 100644 tests/components/motioneye/test_web_hooks.py diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index f766bb86be2..4fb3b2a19c6 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -2,19 +2,46 @@ from __future__ import annotations import asyncio +import json import logging from typing import Any, Callable +from urllib.parse import urlencode, urljoin +from aiohttp.web import Request, Response from motioneye_client.client import ( MotionEyeClient, MotionEyeClientError, MotionEyeClientInvalidAuthError, ) -from motioneye_client.const import KEY_CAMERAS, KEY_ID, KEY_NAME +from motioneye_client.const import ( + KEY_CAMERAS, + KEY_HTTP_METHOD_POST_JSON, + KEY_ID, + KEY_NAME, + KEY_WEB_HOOK_CONVERSION_SPECIFIERS, + KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, + KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, + KEY_WEB_HOOK_NOTIFICATIONS_URL, + KEY_WEB_HOOK_STORAGE_ENABLED, + KEY_WEB_HOOK_STORAGE_HTTP_METHOD, + KEY_WEB_HOOK_STORAGE_URL, +) from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.webhook import ( + async_generate_id, + async_generate_path, + async_register as webhook_register, + async_unregister as webhook_unregister, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_NAME, + CONF_URL, + CONF_WEBHOOK_ID, + HTTP_BAD_REQUEST, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -22,23 +49,35 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.network import get_url from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + ATTR_EVENT_TYPE, + ATTR_WEBHOOK_ID, CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, CONF_CLIENT, CONF_COORDINATOR, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, + CONF_WEBHOOK_SET, + CONF_WEBHOOK_SET_OVERWRITE, DEFAULT_SCAN_INTERVAL, + DEFAULT_WEBHOOK_SET, + DEFAULT_WEBHOOK_SET_OVERWRITE, DOMAIN, + EVENT_FILE_STORED, + EVENT_FILE_STORED_KEYS, + EVENT_MOTION_DETECTED, + EVENT_MOTION_DETECTED_KEYS, MOTIONEYE_MANUFACTURER, SIGNAL_CAMERA_ADD, + WEB_HOOK_SENTINEL_KEY, + WEB_HOOK_SENTINEL_VALUE, ) _LOGGER = logging.getLogger(__name__) - PLATFORMS = [CAMERA_DOMAIN] @@ -97,6 +136,15 @@ def listen_for_new_cameras( ) +@callback +def async_generate_motioneye_webhook(hass: HomeAssistant, webhook_id: str) -> str: + """Generate the full local URL for a webhook_id.""" + return "{}{}".format( + get_url(hass, allow_cloud=False), + async_generate_path(webhook_id), + ) + + @callback def _add_camera( hass: HomeAssistant, @@ -109,13 +157,93 @@ def _add_camera( ) -> None: """Add a motionEye camera to hass.""" - device_registry.async_get_or_create( + def _is_recognized_web_hook(url: str) -> bool: + """Determine whether this integration set a web hook.""" + return f"{WEB_HOOK_SENTINEL_KEY}={WEB_HOOK_SENTINEL_VALUE}" in url + + def _set_webhook( + url: str, + key_url: str, + key_method: str, + key_enabled: str, + camera: dict[str, Any], + ) -> bool: + """Set a web hook.""" + if ( + entry.options.get( + CONF_WEBHOOK_SET_OVERWRITE, + DEFAULT_WEBHOOK_SET_OVERWRITE, + ) + or not camera.get(key_url) + or _is_recognized_web_hook(camera[key_url]) + ) and ( + not camera.get(key_enabled, False) + or camera.get(key_method) != KEY_HTTP_METHOD_POST_JSON + or camera.get(key_url) != url + ): + camera[key_enabled] = True + camera[key_method] = KEY_HTTP_METHOD_POST_JSON + camera[key_url] = url + return True + return False + + def _build_url( + device: dr.DeviceEntry, base: str, event_type: str, keys: list[str] + ) -> str: + """Build a motionEye webhook URL.""" + + # This URL-surgery cannot use YARL because the output must NOT be + # url-encoded. This is because motionEye will do further string + # manipulation/substitution on this value before ultimately fetching it, + # and it cannot deal with URL-encoded input to that string manipulation. + return urljoin( + base, + "?" + + urlencode( + { + **{k: KEY_WEB_HOOK_CONVERSION_SPECIFIERS[k] for k in sorted(keys)}, + WEB_HOOK_SENTINEL_KEY: WEB_HOOK_SENTINEL_VALUE, + ATTR_EVENT_TYPE: event_type, + ATTR_DEVICE_ID: device.id, + }, + safe="%{}", + ), + ) + + device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={device_identifier}, manufacturer=MOTIONEYE_MANUFACTURER, model=MOTIONEYE_MANUFACTURER, name=camera[KEY_NAME], ) + if entry.options.get(CONF_WEBHOOK_SET, DEFAULT_WEBHOOK_SET): + url = async_generate_motioneye_webhook(hass, entry.data[CONF_WEBHOOK_ID]) + + if _set_webhook( + _build_url( + device, + url, + EVENT_MOTION_DETECTED, + EVENT_MOTION_DETECTED_KEYS, + ), + KEY_WEB_HOOK_NOTIFICATIONS_URL, + KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, + KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, + camera, + ) | _set_webhook( + _build_url( + device, + url, + EVENT_FILE_STORED, + EVENT_FILE_STORED_KEYS, + ), + KEY_WEB_HOOK_STORAGE_URL, + KEY_WEB_HOOK_STORAGE_HTTP_METHOD, + KEY_WEB_HOOK_STORAGE_ENABLED, + camera, + ): + hass.async_create_task(client.async_set_camera(camera_id, camera)) async_dispatcher_send( hass, @@ -124,6 +252,11 @@ def _add_camera( ) +async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Handle entry updates.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up motionEye from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -145,6 +278,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await client.async_client_close() raise ConfigEntryNotReady from exc + # Ensure every loaded entry has a registered webhook id. + if CONF_WEBHOOK_ID not in entry.data: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_WEBHOOK_ID: async_generate_id()} + ) + webhook_register( + hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) + @callback async def async_update_data() -> dict[str, Any] | None: try: @@ -196,8 +338,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_identifier, ) - # Ensure every device associated with this config entry is still in the list of - # motionEye cameras, otherwise remove the device (and thus entities). + # Ensure every device associated with this config entry is still in the + # list of motionEye cameras, otherwise remove the device (and thus + # entities). for device_entry in dr.async_entries_for_config_entry( device_registry, entry.entry_id ): @@ -218,6 +361,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator.async_add_listener(_async_process_motioneye_cameras) ) await coordinator.async_refresh() + entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) hass.async_create_task(setup_then_listen()) return True @@ -225,9 +369,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: config_data = hass.data[DOMAIN].pop(entry.entry_id) await config_data[CONF_CLIENT].async_client_close() return unload_ok + + +async def handle_webhook( + hass: HomeAssistant, webhook_id: str, request: Request +) -> None | Response: + """Handle webhook callback.""" + + try: + data = await request.json() + except (json.decoder.JSONDecodeError, UnicodeDecodeError): + return Response( + text="Could not decode request", + status=HTTP_BAD_REQUEST, + ) + + for key in (ATTR_DEVICE_ID, ATTR_EVENT_TYPE): + if key not in data: + return Response( + text=f"Missing webhook parameter: {key}", + status=HTTP_BAD_REQUEST, + ) + + event_type = data[ATTR_EVENT_TYPE] + device_registry = dr.async_get(hass) + device_id = data[ATTR_DEVICE_ID] + device = device_registry.async_get(device_id) + + if not device: + return Response( + text=f"Device not found: {device_id}", + status=HTTP_BAD_REQUEST, + ) + + hass.bus.async_fire( + f"{DOMAIN}.{event_type}", + { + ATTR_DEVICE_ID: device.id, + ATTR_NAME: device.name, + ATTR_WEBHOOK_ID: webhook_id, + **data, + }, + ) + return None diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 463c804028a..d6792bba2a8 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -11,8 +11,14 @@ from motioneye_client.client import ( ) import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow -from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + OptionsFlow, +) +from homeassistant.const import CONF_SOURCE, CONF_URL, CONF_WEBHOOK_ID +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv @@ -22,6 +28,10 @@ from .const import ( CONF_ADMIN_USERNAME, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, + CONF_WEBHOOK_SET, + CONF_WEBHOOK_SET_OVERWRITE, + DEFAULT_WEBHOOK_SET, + DEFAULT_WEBHOOK_SET_OVERWRITE, DOMAIN, ) @@ -122,6 +132,9 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): return _get_form(user_input, errors) if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and reauth_entry is not None: + # Persist the same webhook id across reauths. + if CONF_WEBHOOK_ID in reauth_entry.data: + user_input[CONF_WEBHOOK_ID] = reauth_entry.data[CONF_WEBHOOK_ID] self.hass.config_entries.async_update_entry(reauth_entry, data=user_input) # Need to manually reload, as the listener won't have been # installed because the initial load did not succeed (the reauth @@ -167,3 +180,43 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> MotionEyeOptionsFlow: + """Get the Hyperion Options flow.""" + return MotionEyeOptionsFlow(config_entry) + + +class MotionEyeOptionsFlow(OptionsFlow): + """motionEye options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize a motionEye options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + schema: dict[vol.Marker, type] = { + vol.Required( + CONF_WEBHOOK_SET, + default=self._config_entry.options.get( + CONF_WEBHOOK_SET, + DEFAULT_WEBHOOK_SET, + ), + ): bool, + vol.Required( + CONF_WEBHOOK_SET_OVERWRITE, + default=self._config_entry.options.get( + CONF_WEBHOOK_SET_OVERWRITE, + DEFAULT_WEBHOOK_SET_OVERWRITE, + ), + ): bool, + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index fbd0d9b4d2e..d918ca5ec23 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -1,19 +1,89 @@ """Constants for the motionEye integration.""" from datetime import timedelta +from typing import Final -DOMAIN = "motioneye" +from motioneye_client.const import ( + KEY_WEB_HOOK_CS_CAMERA_ID, + KEY_WEB_HOOK_CS_CHANGED_PIXELS, + KEY_WEB_HOOK_CS_DESPECKLE_LABELS, + KEY_WEB_HOOK_CS_EVENT, + KEY_WEB_HOOK_CS_FILE_PATH, + KEY_WEB_HOOK_CS_FILE_TYPE, + KEY_WEB_HOOK_CS_FPS, + KEY_WEB_HOOK_CS_FRAME_NUMBER, + KEY_WEB_HOOK_CS_HEIGHT, + KEY_WEB_HOOK_CS_HOST, + KEY_WEB_HOOK_CS_MOTION_CENTER_X, + KEY_WEB_HOOK_CS_MOTION_CENTER_Y, + KEY_WEB_HOOK_CS_MOTION_HEIGHT, + KEY_WEB_HOOK_CS_MOTION_VERSION, + KEY_WEB_HOOK_CS_MOTION_WIDTH, + KEY_WEB_HOOK_CS_NOISE_LEVEL, + KEY_WEB_HOOK_CS_THRESHOLD, + KEY_WEB_HOOK_CS_WIDTH, +) -CONF_CLIENT = "client" -CONF_COORDINATOR = "coordinator" -CONF_ADMIN_PASSWORD = "admin_password" -CONF_ADMIN_USERNAME = "admin_username" -CONF_SURVEILLANCE_USERNAME = "surveillance_username" -CONF_SURVEILLANCE_PASSWORD = "surveillance_password" -DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) +DOMAIN: Final = "motioneye" -MOTIONEYE_MANUFACTURER = "motionEye" +ATTR_EVENT_TYPE: Final = "event_type" +ATTR_WEBHOOK_ID: Final = "webhook_id" -SIGNAL_CAMERA_ADD = f"{DOMAIN}_camera_add_signal." "{}" -SIGNAL_CAMERA_REMOVE = f"{DOMAIN}_camera_remove_signal." "{}" +CONF_CLIENT: Final = "client" +CONF_COORDINATOR: Final = "coordinator" +CONF_ADMIN_PASSWORD: Final = "admin_password" +CONF_ADMIN_USERNAME: Final = "admin_username" +CONF_SURVEILLANCE_USERNAME: Final = "surveillance_username" +CONF_SURVEILLANCE_PASSWORD: Final = "surveillance_password" +CONF_WEBHOOK_SET: Final = "webhook_set" +CONF_WEBHOOK_SET_OVERWRITE: Final = "webhook_set_overwrite" -TYPE_MOTIONEYE_MJPEG_CAMERA = "motioneye_mjpeg_camera" +DEFAULT_WEBHOOK_SET: Final = True +DEFAULT_WEBHOOK_SET_OVERWRITE: Final = False +DEFAULT_SCAN_INTERVAL: Final = timedelta(seconds=30) + +EVENT_MOTION_DETECTED: Final = "motion_detected" +EVENT_FILE_STORED: Final = "file_stored" + +EVENT_MOTION_DETECTED_KEYS: Final = [ + KEY_WEB_HOOK_CS_EVENT, + KEY_WEB_HOOK_CS_FRAME_NUMBER, + KEY_WEB_HOOK_CS_CAMERA_ID, + KEY_WEB_HOOK_CS_CHANGED_PIXELS, + KEY_WEB_HOOK_CS_NOISE_LEVEL, + KEY_WEB_HOOK_CS_WIDTH, + KEY_WEB_HOOK_CS_HEIGHT, + KEY_WEB_HOOK_CS_MOTION_WIDTH, + KEY_WEB_HOOK_CS_MOTION_HEIGHT, + KEY_WEB_HOOK_CS_MOTION_CENTER_X, + KEY_WEB_HOOK_CS_MOTION_CENTER_Y, + KEY_WEB_HOOK_CS_THRESHOLD, + KEY_WEB_HOOK_CS_DESPECKLE_LABELS, + KEY_WEB_HOOK_CS_FPS, + KEY_WEB_HOOK_CS_HOST, + KEY_WEB_HOOK_CS_MOTION_VERSION, +] + +EVENT_FILE_STORED_KEYS: Final = [ + KEY_WEB_HOOK_CS_EVENT, + KEY_WEB_HOOK_CS_FRAME_NUMBER, + KEY_WEB_HOOK_CS_CAMERA_ID, + KEY_WEB_HOOK_CS_NOISE_LEVEL, + KEY_WEB_HOOK_CS_WIDTH, + KEY_WEB_HOOK_CS_HEIGHT, + KEY_WEB_HOOK_CS_FILE_PATH, + KEY_WEB_HOOK_CS_FILE_TYPE, + KEY_WEB_HOOK_CS_THRESHOLD, + KEY_WEB_HOOK_CS_FPS, + KEY_WEB_HOOK_CS_HOST, + KEY_WEB_HOOK_CS_MOTION_VERSION, +] + +MOTIONEYE_MANUFACTURER: Final = "motionEye" + +SIGNAL_CAMERA_ADD: Final = f"{DOMAIN}_camera_add_signal." "{}" +SIGNAL_CAMERA_REMOVE: Final = f"{DOMAIN}_camera_remove_signal." "{}" + +TYPE_MOTIONEYE_MJPEG_CAMERA: Final = "motioneye_mjpeg_camera" + +WEB_HOOK_SENTINEL_KEY: Final = "src" +WEB_HOOK_SENTINEL_VALUE: Final = "hass-motioneye" diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 43cb231c30c..4d1863c8e6a 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -3,8 +3,12 @@ "name": "motionEye", "documentation": "https://www.home-assistant.io/integrations/motioneye", "config_flow": true, + "dependencies": [ + "http", + "webhook" + ], "requirements": [ - "motioneye-client==0.3.6" + "motioneye-client==0.3.9" ], "codeowners": [ "@dermotduffy" diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index d89b5cab275..9763e1caf34 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -25,5 +25,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Configure motionEye webhooks to report events to Home Assistant", + "webhook_set_overwrite": "Overwrite unrecognized webhooks" + } + } + } } } diff --git a/requirements_all.txt b/requirements_all.txt index 674956249fb..6a94ee00504 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -985,7 +985,7 @@ mitemp_bt==0.0.3 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.6 +motioneye-client==0.3.9 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ca5677143e..a442c1a72fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -554,7 +554,7 @@ minio==4.0.9 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.6 +motioneye-client==0.3.9 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index ed91d7c40a3..8db3736aaef 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, Mock, patch from motioneye_client.const import DEFAULT_PORT from homeassistant.components.motioneye.const import DOMAIN +from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant @@ -151,14 +152,14 @@ def create_mock_motioneye_config_entry( options: dict[str, Any] | None = None, ) -> ConfigEntry: """Add a test config entry.""" - config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + config_entry: MockConfigEntry = MockConfigEntry( entry_id=TEST_CONFIG_ENTRY_ID, domain=DOMAIN, data=data or {CONF_URL: TEST_URL}, title=f"{TEST_URL}", options=options or {}, ) - config_entry.add_to_hass(hass) # type: ignore[no-untyped-call] + config_entry.add_to_hass(hass) return config_entry @@ -167,7 +168,13 @@ async def setup_mock_motioneye_config_entry( config_entry: ConfigEntry | None = None, client: Mock | None = None, ) -> ConfigEntry: - """Add a mock MotionEye config entry to hass.""" + """Create and setup a mock motionEye config entry.""" + + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com"}, + ) + config_entry = config_entry or create_mock_motioneye_config_entry(hass) client = client or create_mock_motioneye_client() diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index f1ddcea4386..af2fd3c365a 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -1,7 +1,7 @@ """Test the motionEye camera.""" import copy import logging -from typing import Any +from typing import Any, cast from unittest.mock import AsyncMock, Mock from aiohttp import web @@ -235,7 +235,7 @@ async def test_get_still_image_from_camera( # It won't actually get a stream from the dummy handler, so just catch # the expected exception, then verify the right handler was called. with pytest.raises(HomeAssistantError): - await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=None) # type: ignore[no-untyped-call] + await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=1) assert image_handler.called @@ -269,7 +269,9 @@ async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) # It won't actually get a stream from the dummy handler, so just catch # the expected exception, then verify the right handler was called. with pytest.raises(HTTPBadGateway): - await async_get_mjpeg_stream(hass, None, TEST_CAMERA_ENTITY_ID) # type: ignore[no-untyped-call] + await async_get_mjpeg_stream( + hass, cast(web.Request, None), TEST_CAMERA_ENTITY_ID + ) assert stream_handler.called diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index fbdabdadb41..604085cec8f 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -14,9 +14,11 @@ from homeassistant.components.motioneye.const import ( CONF_ADMIN_USERNAME, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, + CONF_WEBHOOK_SET, + CONF_WEBHOOK_SET_OVERWRITE, DOMAIN, ) -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from . import TEST_URL, create_mock_motioneye_client, create_mock_motioneye_config_entry @@ -247,6 +249,7 @@ async def test_reauth(hass: HomeAssistant) -> None: """Test a reauth.""" config_data = { CONF_URL: TEST_URL, + CONF_WEBHOOK_ID: "test-webhook-id", } config_entry = create_mock_motioneye_config_entry(hass, data=config_data) @@ -287,7 +290,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" - assert dict(config_entry.data) == new_data + assert dict(config_entry.data) == {**new_data, CONF_WEBHOOK_ID: "test-webhook-id"} assert len(mock_setup_entry.mock_calls) == 1 assert mock_client.async_client_close.called @@ -300,11 +303,11 @@ async def test_duplicate(hass: HomeAssistant) -> None: } # Add an existing entry with the same URL. - existing_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + existing_entry: MockConfigEntry = MockConfigEntry( domain=DOMAIN, data=config_data, ) - existing_entry.add_to_hass(hass) # type: ignore[no-untyped-call] + existing_entry.add_to_hass(hass) # Now do the usual config entry process, and verify it is rejected. create_mock_motioneye_config_entry(hass, data=config_data) @@ -431,3 +434,35 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 + + +async def test_options(hass: HomeAssistant) -> None: + """Check an options flow.""" + + config_entry = create_mock_motioneye_config_entry(hass) + + client = create_mock_motioneye_client() + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ): + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_WEBHOOK_SET: True, + CONF_WEBHOOK_SET_OVERWRITE: True, + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_WEBHOOK_SET] + assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py new file mode 100644 index 00000000000..03b4e8bc46a --- /dev/null +++ b/tests/components/motioneye/test_web_hooks.py @@ -0,0 +1,348 @@ +"""Test the motionEye camera web hooks.""" +import copy +import logging +from typing import Any +from unittest.mock import AsyncMock, call, patch + +from motioneye_client.const import ( + KEY_CAMERAS, + KEY_HTTP_METHOD_POST_JSON, + KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, + KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, + KEY_WEB_HOOK_NOTIFICATIONS_URL, + KEY_WEB_HOOK_STORAGE_ENABLED, + KEY_WEB_HOOK_STORAGE_HTTP_METHOD, + KEY_WEB_HOOK_STORAGE_URL, +) + +from homeassistant.components.motioneye.const import ( + ATTR_EVENT_TYPE, + CONF_WEBHOOK_SET_OVERWRITE, + DOMAIN, + EVENT_FILE_STORED, + EVENT_MOTION_DETECTED, +) +from homeassistant.components.webhook import URL_WEBHOOK_PATH +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_URL, + CONF_WEBHOOK_ID, + HTTP_BAD_REQUEST, + HTTP_OK, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import ( + TEST_CAMERA, + TEST_CAMERA_DEVICE_IDENTIFIER, + TEST_CAMERA_ID, + TEST_CAMERA_NAME, + TEST_CAMERAS, + TEST_URL, + create_mock_motioneye_client, + create_mock_motioneye_config_entry, + setup_mock_motioneye_config_entry, +) + +from tests.common import async_capture_events + +_LOGGER = logging.getLogger(__name__) + + +WEB_HOOK_MOTION_DETECTED_QUERY_STRING = ( + "camera_id=%t&changed_pixels=%D&despeckle_labels=%Q&event=%v&fps=%{fps}" + "&frame_number=%q&height=%h&host=%{host}&motion_center_x=%K&motion_center_y=%L" + "&motion_height=%J&motion_version=%{ver}&motion_width=%i&noise_level=%N" + "&threshold=%o&width=%w&src=hass-motioneye&event_type=motion_detected" +) + +WEB_HOOK_FILE_STORED_QUERY_STRING = ( + "camera_id=%t&event=%v&file_path=%f&file_type=%n&fps=%{fps}&frame_number=%q" + "&height=%h&host=%{host}&motion_version=%{ver}&noise_level=%N&threshold=%o&width=%w" + "&src=hass-motioneye&event_type=file_stored" +) + + +async def test_setup_camera_without_webhook(hass: HomeAssistant) -> None: + """Test a camera with no webhook.""" + client = create_mock_motioneye_client() + config_entry = await setup_mock_motioneye_config_entry(hass, client=client) + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_device( + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} + ) + assert device + + expected_camera = copy.deepcopy(TEST_CAMERA) + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" + ) + + expected_camera[KEY_WEB_HOOK_STORAGE_ENABLED] = True + expected_camera[KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON + expected_camera[KEY_WEB_HOOK_STORAGE_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" + ) + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera) + + +async def test_setup_camera_with_wrong_webhook( + hass: HomeAssistant, +) -> None: + """Test camera with wrong web hook.""" + wrong_url = "http://wrong-url" + + client = create_mock_motioneye_client() + cameras = copy.deepcopy(TEST_CAMERAS) + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_URL] = wrong_url + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_URL] = wrong_url + client.async_get_cameras = AsyncMock(return_value=cameras) + + config_entry = create_mock_motioneye_config_entry(hass) + await setup_mock_motioneye_config_entry( + hass, + config_entry=config_entry, + client=client, + ) + assert not client.async_set_camera.called + + # Update the options, which will trigger a reload with the new behavior. + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=client, + ): + hass.config_entries.async_update_entry( + config_entry, options={CONF_WEBHOOK_SET_OVERWRITE: True} + ) + await hass.async_block_till_done() + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_device( + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} + ) + assert device + + expected_camera = copy.deepcopy(TEST_CAMERA) + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" + ) + + expected_camera[KEY_WEB_HOOK_STORAGE_ENABLED] = True + expected_camera[KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON + expected_camera[KEY_WEB_HOOK_STORAGE_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" + ) + + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera) + + +async def test_setup_camera_with_old_webhook( + hass: HomeAssistant, +) -> None: + """Verify that webhooks are overwritten if they are from this integration. + + Even if the overwrite option is disabled, verify the behavior is still to + overwrite incorrect versions of the URL that were set by this integration. + + (To allow the web hook URL to be seamlessly updated in future versions) + """ + + old_url = "http://old-url?src=hass-motioneye" + + client = create_mock_motioneye_client() + cameras = copy.deepcopy(TEST_CAMERAS) + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_URL] = old_url + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_URL] = old_url + client.async_get_cameras = AsyncMock(return_value=cameras) + + config_entry = create_mock_motioneye_config_entry(hass) + await setup_mock_motioneye_config_entry( + hass, + config_entry=config_entry, + client=client, + ) + assert client.async_set_camera.called + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_device( + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} + ) + assert device + + expected_camera = copy.deepcopy(TEST_CAMERA) + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON + expected_camera[KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" + ) + + expected_camera[KEY_WEB_HOOK_STORAGE_ENABLED] = True + expected_camera[KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = KEY_HTTP_METHOD_POST_JSON + expected_camera[KEY_WEB_HOOK_STORAGE_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" + ) + + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera) + + +async def test_setup_camera_with_correct_webhook( + hass: HomeAssistant, +) -> None: + """Verify that webhooks are not overwritten if they are already correct.""" + + client = create_mock_motioneye_client() + config_entry = create_mock_motioneye_config_entry( + hass, data={CONF_URL: TEST_URL, CONF_WEBHOOK_ID: "webhook_secret_id"} + ) + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + cameras = copy.deepcopy(TEST_CAMERAS) + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True + cameras[KEY_CAMERAS][0][ + KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD + ] = KEY_HTTP_METHOD_POST_JSON + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" + ) + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_ENABLED] = True + cameras[KEY_CAMERAS][0][ + KEY_WEB_HOOK_STORAGE_HTTP_METHOD + ] = KEY_HTTP_METHOD_POST_JSON + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_URL] = ( + "https://example.com" + + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + + f"?{WEB_HOOK_FILE_STORED_QUERY_STRING}&device_id={device.id}" + ) + client.async_get_cameras = AsyncMock(return_value=cameras) + + await setup_mock_motioneye_config_entry( + hass, + config_entry=config_entry, + client=client, + ) + + # Webhooks are correctly configured, so no set call should have been made. + assert not client.async_set_camera.called + + +async def test_good_query(hass: HomeAssistant, aiohttp_client: Any) -> None: + """Test good callbacks.""" + await async_setup_component(hass, "http", {"http": {}}) + + device_registry = await dr.async_get_registry(hass) + client = create_mock_motioneye_client() + config_entry = await setup_mock_motioneye_config_entry(hass, client=client) + + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, + ) + + data = { + "one": "1", + "two": "2", + ATTR_DEVICE_ID: device.id, + } + client = await aiohttp_client(hass.http.app) + + for event in (EVENT_MOTION_DETECTED, EVENT_FILE_STORED): + events = async_capture_events(hass, f"{DOMAIN}.{event}") + + resp = await client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + **data, + ATTR_EVENT_TYPE: event, + }, + ) + assert resp.status == HTTP_OK + + assert len(events) == 1 + assert events[0].data == { + "name": TEST_CAMERA_NAME, + "device_id": device.id, + ATTR_EVENT_TYPE: event, + CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID], + **data, + } + + +async def test_bad_query_missing_parameters( + hass: HomeAssistant, aiohttp_client: Any +) -> None: + """Test a query with missing parameters.""" + await async_setup_component(hass, "http", {"http": {}}) + config_entry = await setup_mock_motioneye_config_entry(hass) + + client = await aiohttp_client(hass.http.app) + + resp = await client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), json={} + ) + assert resp.status == HTTP_BAD_REQUEST + + +async def test_bad_query_no_such_device( + hass: HomeAssistant, aiohttp_client: Any +) -> None: + """Test a correct query with incorrect device.""" + await async_setup_component(hass, "http", {"http": {}}) + config_entry = await setup_mock_motioneye_config_entry(hass) + + client = await aiohttp_client(hass.http.app) + + resp = await client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + json={ + ATTR_EVENT_TYPE: EVENT_MOTION_DETECTED, + ATTR_DEVICE_ID: "not-a-real-device", + }, + ) + assert resp.status == HTTP_BAD_REQUEST + + +async def test_bad_query_cannot_decode( + hass: HomeAssistant, aiohttp_client: Any +) -> None: + """Test a correct query with incorrect device.""" + await async_setup_component(hass, "http", {"http": {}}) + config_entry = await setup_mock_motioneye_config_entry(hass) + + client = await aiohttp_client(hass.http.app) + + motion_events = async_capture_events(hass, f"{DOMAIN}.{EVENT_MOTION_DETECTED}") + storage_events = async_capture_events(hass, f"{DOMAIN}.{EVENT_FILE_STORED}") + + resp = await client.post( + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), + data=b"this is not json", + ) + assert resp.status == HTTP_BAD_REQUEST + assert not motion_events + assert not storage_events From 60ea219101f84898b4ff8bbdd1b03e4f2f530b5c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Jul 2021 04:24:19 -0500 Subject: [PATCH 015/818] Remove redundant property definitions in Notion (#52367) * Remove redundant property definitions in Notion * Code review --- homeassistant/components/notion/__init__.py | 74 ++++++------------- .../components/notion/binary_sensor.py | 31 ++++---- homeassistant/components/notion/sensor.py | 16 +--- 3 files changed, 38 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 141086bb2be..294d93a30e2 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -14,7 +14,6 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -34,14 +33,10 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the Notion component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Notion as a config entry.""" + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) + if not entry.unique_id: hass.config_entries.async_update_entry( entry, unique_id=entry.data[CONF_USERNAME] @@ -144,44 +139,12 @@ class NotionEntity(CoordinatorEntity): ) -> None: """Initialize the entity.""" super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._bridge_id = bridge_id - self._device_class = device_class - self._name = name - self._sensor_id = sensor_id - self._state = None - self._system_id = system_id - self._unique_id = ( - f'{sensor_id}_{self.coordinator.data["tasks"][task_id]["task_type"]}' - ) - self.task_id = task_id - @property - def available(self) -> bool: - """Return True if entity is available.""" - return ( - self.coordinator.last_update_success - and self.task_id in self.coordinator.data["tasks"] - and self._state - ) + self._attr_device_class = device_class - @property - def device_class(self) -> str: - """Return the device class.""" - return self._device_class - - @property - def extra_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def device_info(self) -> DeviceInfo: - """Return device registry information for this entity.""" - bridge = self.coordinator.data["bridges"].get(self._bridge_id, {}) - sensor = self.coordinator.data["sensors"][self._sensor_id] - - return { + bridge = self.coordinator.data["bridges"].get(bridge_id, {}) + sensor = self.coordinator.data["sensors"][sensor_id] + self._attr_device_info = { "identifiers": {(DOMAIN, sensor["hardware_id"])}, "manufacturer": "Silicon Labs", "model": sensor["hardware_revision"], @@ -190,16 +153,23 @@ class NotionEntity(CoordinatorEntity): "via_device": (DOMAIN, bridge.get("hardware_id")), } - @property - def name(self) -> str: - """Return the name of the entity.""" - sensor = self.coordinator.data["sensors"][self._sensor_id] - return f'{sensor["name"]}: {self._name}' + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_name = f'{sensor["name"]}: {name}' + self._attr_unique_id = ( + f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}' + ) + self._bridge_id = bridge_id + self._sensor_id = sensor_id + self._system_id = system_id + self._task_id = task_id @property - def unique_id(self) -> str: - """Return a unique, unchanging string that represents this entity.""" - return self._unique_id + def available(self) -> bool: + """Return True if entity is available.""" + return ( + self.coordinator.last_update_success + and self._task_id in self.coordinator.data["tasks"] + ) async def _async_update_bridge_id(self) -> None: """Update the entity's bridge ID if it has changed. @@ -240,7 +210,7 @@ class NotionEntity(CoordinatorEntity): @callback def _handle_coordinator_update(self): """Respond to a DataUpdateCoordinator update.""" - if self.task_id in self.coordinator.data["tasks"]: + if self._task_id in self.coordinator.data["tasks"]: self.hass.async_create_task(self._async_update_bridge_id()) self._async_update_from_latest_data() diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 168e35a3a97..2604e2bdf58 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -77,24 +77,19 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self.coordinator.data["tasks"][self.task_id] + task = self.coordinator.data["tasks"][self._task_id] if "value" in task["status"]: - self._state = task["status"]["value"] + state = task["status"]["value"] elif task["status"].get("insights", {}).get("primary"): - self._state = task["status"]["insights"]["primary"]["to_state"] + state = task["status"]["insights"]["primary"]["to_state"] else: LOGGER.warning("Unknown data payload: %s", task["status"]) - self._state = None - - @property - def is_on(self) -> bool: - """Return whether the sensor is on or off.""" - task = self.coordinator.data["tasks"][self.task_id] + state = None if task["task_type"] == SENSOR_BATTERY: - return self._state == "critical" - if task["task_type"] in ( + self._attr_is_on = state == "critical" + elif task["task_type"] in ( SENSOR_DOOR, SENSOR_GARAGE_DOOR, SENSOR_SAFE, @@ -102,10 +97,10 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): SENSOR_WINDOW_HINGED_HORIZONTAL, SENSOR_WINDOW_HINGED_VERTICAL, ): - return self._state != "closed" - if task["task_type"] == SENSOR_LEAK: - return self._state != "no_leak" - if task["task_type"] == SENSOR_MISSING: - return self._state == "not_missing" - if task["task_type"] == SENSOR_SMOKE_CO: - return self._state != "no_alarm" + self._attr_is_on = state != "closed" + elif task["task_type"] == SENSOR_LEAK: + self._attr_is_on = state != "no_leak" + elif task["task_type"] == SENSOR_MISSING: + self._attr_is_on = state == "not_missing" + elif task["task_type"] == SENSOR_SMOKE_CO: + self._attr_is_on = state != "no_alarm" diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 2494ed2d2e8..650b884bf27 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -61,25 +61,15 @@ class NotionSensor(NotionEntity, SensorEntity): coordinator, task_id, sensor_id, bridge_id, system_id, name, device_class ) - self._unit = unit - - @property - def state(self) -> str: - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return self._unit + self._attr_unit_of_measurement = unit @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self.coordinator.data["tasks"][self.task_id] + task = self.coordinator.data["tasks"][self._task_id] if task["task_type"] == SENSOR_TEMPERATURE: - self._state = round(float(task["status"]["value"]), 1) + self._attr_state = round(float(task["status"]["value"]), 1) else: LOGGER.error( "Unknown task type: %s: %s", From c1c078c3d5dc411404230bccf89dc08acbba4b96 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 1 Jul 2021 12:14:37 +0200 Subject: [PATCH 016/818] Bump pyfritzhome to 6.2.0 (#52345) --- homeassistant/components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 3daecb1980d..c1db226d348 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.4.2"], + "requirements": ["pyfritzhome==0.6.2"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index 6a94ee00504..9ed9b4181a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1454,7 +1454,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.4.2 +pyfritzhome==0.6.2 # homeassistant.components.fronius pyfronius==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a442c1a72fd..bf8d9fd8138 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -813,7 +813,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.4.2 +pyfritzhome==0.6.2 # homeassistant.components.ifttt pyfttt==0.3 From 451b976458b7cef740e80349b1e69e58eae4dfd6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Jul 2021 12:43:37 +0200 Subject: [PATCH 017/818] Demo: Explicitly return None when no extra state attribute set (#52390) --- homeassistant/components/demo/remote.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 1badd391575..c8e54aa65f3 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -53,6 +53,7 @@ class DemoRemote(RemoteEntity): """Return device state attributes.""" if self._last_command_sent is not None: return {"last_command_sent": self._last_command_sent} + return None def turn_on(self, **kwargs: Any) -> None: """Turn the remote on.""" From 7f309b4e6e4e8917f7a64521b1c86c5174bd3b29 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 1 Jul 2021 07:34:09 -0400 Subject: [PATCH 018/818] Add support for Formaldehyde and VOC level sensors (#52232) --- .../zha/core/channels/measurement.py | 14 ++++++++++++ .../components/zha/core/registries.py | 3 +++ homeassistant/components/zha/sensor.py | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 99d062d4c3e..77351441b56 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -102,3 +102,17 @@ class CarbonDioxideConcentration(ZigbeeChannel): "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), } ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.FormaldehydeConcentration.cluster_id +) +class FormaldehydeConcentration(ZigbeeChannel): + """Formaldehyde measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), + } + ] diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 5fe7f806355..6bb9e155b0f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -32,6 +32,7 @@ PHILLIPS_REMOTE_CLUSTER = 0xFC00 SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 +VOC_LEVEL_CLUSTER = 0x042E REMOTE_DEVICE_TYPES = { zigpy.profiles.zha.PROFILE_ID: [ @@ -62,6 +63,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { # a different dict that is keyed by manufacturer SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, + VOC_LEVEL_CLUSTER: SENSOR, zcl.clusters.closures.DoorLock.cluster_id: LOCK, zcl.clusters.closures.WindowCovering.cluster_id: COVER, zcl.clusters.general.AnalogInput.cluster_id: SENSOR, @@ -73,6 +75,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.hvac.Fan.cluster_id: FAN, zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: SENSOR, zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: SENSOR, + zcl.clusters.measurement.FormaldehydeConcentration.cluster_id: SENSOR, zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR, zcl.clusters.measurement.OccupancySensing.cluster_id: BINARY_SENSOR, zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 714df80eebb..b9a86b79063 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -20,6 +20,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, @@ -319,3 +320,24 @@ class CarbonMonoxideConcentration(Sensor): _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION + + +@STRICT_MATCH(generic_ids="channel_0x042e") +@STRICT_MATCH(channel_names="voc_level") +class VOCLevel(Sensor): + """VOC Level sensor.""" + + SENSOR_ATTR = "measured_value" + _decimals = 0 + _multiplier = 1e6 + _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + + +@STRICT_MATCH(channel_names="formaldehyde_concentration") +class FormaldehydeConcentration(Sensor): + """Formaldehyde Concentration sensor.""" + + SENSOR_ATTR = "measured_value" + _decimals = 0 + _multiplier = 1e6 + _unit = CONCENTRATION_PARTS_PER_MILLION From 2097ab76f5c047f134b576f4dfa9283abece8532 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Jul 2021 14:09:48 +0200 Subject: [PATCH 019/818] Allow combining value_template and position_template for template cover (#52383) --- homeassistant/components/template/cover.py | 26 ++-- tests/components/template/test_cover.py | 172 +++++++++++++++------ 2 files changed, 137 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index d985473792e..a9f28d56669 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -62,8 +62,7 @@ POSITION_ACTION = "set_cover_position" TILT_ACTION = "set_cover_tilt_position" CONF_TILT_OPTIMISTIC = "tilt_optimistic" -CONF_VALUE_OR_POSITION_TEMPLATE = "value_or_position" -CONF_OPEN_OR_CLOSE = "open_or_close" +CONF_OPEN_AND_CLOSE = "open_or_close" TILT_FEATURES = ( SUPPORT_OPEN_TILT @@ -76,15 +75,10 @@ COVER_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), vol.Schema( { - vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA, - vol.Inclusive(CLOSE_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA, + vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, - vol.Exclusive( - CONF_POSITION_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE - ): cv.template, - vol.Exclusive( - CONF_VALUE_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE - ): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, @@ -258,10 +252,11 @@ class CoverTemplate(TemplateEntity, CoverEntity): state = str(result).lower() if state in _VALID_STATES: - if state in ("true", STATE_OPEN): - self._position = 100 - else: - self._position = 0 + if not self._position_template: + if state in ("true", STATE_OPEN): + self._position = 100 + else: + self._position = 0 self._is_opening = state == STATE_OPENING self._is_closing = state == STATE_CLOSING @@ -271,7 +266,8 @@ class CoverTemplate(TemplateEntity, CoverEntity): state, ", ".join(_VALID_STATES), ) - self._position = None + if not self._position_template: + self._position = None @callback def _update_position(self, result): diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index c1309a16e67..e2b65abcf25 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -34,7 +34,7 @@ def calls_fixture(hass): return async_mock_service(hass, "test", "automation") -async def test_template_state_text(hass, calls): +async def test_template_state_text(hass, calls, caplog): """Test the state text of a template.""" with assert_setup_component(1, "cover"): assert await setup.async_setup_component( @@ -64,30 +64,147 @@ async def test_template_state_text(hass, calls): await hass.async_start() await hass.async_block_till_done() - state = hass.states.async_set("cover.test_state", STATE_OPEN) + hass.states.async_set("cover.test_state", STATE_OPEN) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") assert state.state == STATE_OPEN - state = hass.states.async_set("cover.test_state", STATE_CLOSED) + hass.states.async_set("cover.test_state", STATE_CLOSED) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") assert state.state == STATE_CLOSED - state = hass.states.async_set("cover.test_state", STATE_OPENING) + hass.states.async_set("cover.test_state", STATE_OPENING) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") assert state.state == STATE_OPENING - state = hass.states.async_set("cover.test_state", STATE_CLOSING) + hass.states.async_set("cover.test_state", STATE_CLOSING) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") assert state.state == STATE_CLOSING + # Unknown state sets position to None - "closing" takes precedence + state = hass.states.async_set("cover.test_state", "dog") + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_CLOSING + assert "Received invalid cover is_on state: dog" in caplog.text + + # Set state to open + hass.states.async_set("cover.test_state", STATE_OPEN) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + + # Unknown state sets position to None -> Open + state = hass.states.async_set("cover.test_state", "cat") + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + assert "Received invalid cover is_on state: cat" in caplog.text + + # Set state to closed + hass.states.async_set("cover.test_state", STATE_CLOSED) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_CLOSED + + # Unknown state sets position to None -> Open + state = hass.states.async_set("cover.test_state", "bear") + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + assert "Received invalid cover is_on state: bear" in caplog.text + + +async def test_template_state_text_combined(hass, calls, caplog): + """Test the state text of a template which combines position and value templates.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "position_template": "{{ states.cover.test.attributes.position }}", + "value_template": "{{ states.cover.test_state.state }}", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # Test default state + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + + # Change to "open" should be ignored + state = hass.states.async_set("cover.test_state", STATE_OPEN) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + + # Change to "closed" should be ignored + state = hass.states.async_set("cover.test_state", STATE_CLOSED) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + + # Change to "opening" should be accepted + state = hass.states.async_set("cover.test_state", STATE_OPENING) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPENING + + # Change to "closing" should be accepted + state = hass.states.async_set("cover.test_state", STATE_CLOSING) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_CLOSING + + # Set position to 0=closed + hass.states.async_set("cover.test", STATE_CLOSED, attributes={"position": 0}) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_CLOSING + assert state.attributes["current_position"] == 0 + + # Clear "closing" state, STATE_OPEN will be ignored and state derived from position + state = hass.states.async_set("cover.test_state", STATE_OPEN) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_CLOSED + + # Set position to 10 + hass.states.async_set("cover.test", STATE_CLOSED, attributes={"position": 10}) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + assert state.attributes["current_position"] == 10 + + # Unknown state should be ignored + state = hass.states.async_set("cover.test_state", "dog") + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPEN + assert state.attributes["current_position"] == 10 + assert "Received invalid cover is_on state: dog" in caplog.text + async def test_template_state_boolean(hass, calls): """Test the value_template attribute.""" @@ -250,43 +367,6 @@ async def test_template_out_of_bounds(hass, calls): assert state.attributes.get("current_position") is None -async def test_template_mutex(hass, calls): - """Test that only value or position template can be used.""" - with assert_setup_component(0, "cover"): - assert await setup.async_setup_component( - hass, - "cover", - { - "cover": { - "platform": "template", - "covers": { - "test_template_cover": { - "value_template": "{{ 1 == 1 }}", - "position_template": "{{ 42 }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, - "icon_template": "{% if states.cover.test_state.state %}" - "mdi:check" - "{% endif %}", - } - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - assert hass.states.async_all() == [] - - async def test_template_open_or_position(hass, caplog): """Test that at least one of open_cover or set_position is used.""" assert await setup.async_setup_component( From 57fbb1c3d95a74bdef12ccb4465834a18522b3a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Jul 2021 14:53:03 +0200 Subject: [PATCH 020/818] Fix sensor statistics collection with empty states (#52393) --- homeassistant/components/sensor/recorder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index ac26e06e07d..c7f3e6e3ecd 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -153,13 +153,15 @@ def _normalize_states( entity_history: list[State], device_class: str, entity_id: str ) -> tuple[str | None, list[tuple[float, State]]]: """Normalize units.""" + unit = None if device_class not in UNIT_CONVERSIONS: # We're not normalizing this device class, return the state as they are fstates = [ (float(el.state), el) for el in entity_history if _is_number(el.state) ] - unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if fstates: + unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return unit, fstates fstates = [] From 312531988a185608c79ed6b2cb1e27e13211f14f Mon Sep 17 00:00:00 2001 From: posixx <2280400+posixx@users.noreply.github.com> Date: Thu, 1 Jul 2021 17:26:32 +0200 Subject: [PATCH 021/818] Vacation Mode on Alarm Panels (#45980) Co-authored-by: Nathan Tilley Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Khole Jones Co-authored-by: Erik Montnemery --- .../alarm_control_panel/__init__.py | 16 +++++++ .../components/alarm_control_panel/const.py | 2 + .../alarm_control_panel/device_action.py | 7 +++ .../alarm_control_panel/device_condition.py | 8 ++++ .../alarm_control_panel/device_trigger.py | 12 ++++++ .../components/alarm_control_panel/group.py | 2 + .../alarm_control_panel/reproduce_state.py | 5 +++ .../alarm_control_panel/services.yaml | 14 ++++++ .../alarm_control_panel/strings.json | 8 +++- homeassistant/const.py | 2 + .../alarm_control_panel/test_device_action.py | 29 ++++++++++++- .../test_device_condition.py | 43 ++++++++++++++++++- .../test_device_trigger.py | 35 ++++++++++++++- .../test_reproduce_state.py | 22 +++++++++- .../components/device_automation/test_init.py | 5 ++- .../test/alarm_control_panel.py | 8 ++++ 16 files changed, 207 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index c8da648fec6..1032150649e 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) @@ -30,6 +31,7 @@ from .const import ( SUPPORT_ALARM_ARM_CUSTOM_BYPASS, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_TRIGGER, ) @@ -81,6 +83,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_alarm_arm_night", [SUPPORT_ALARM_ARM_NIGHT], ) + component.async_register_entity_service( + SERVICE_ALARM_ARM_VACATION, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_vacation", + [SUPPORT_ALARM_ARM_VACATION], + ) component.async_register_entity_service( SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, @@ -164,6 +172,14 @@ class AlarmControlPanelEntity(Entity): """Send arm night command.""" await self.hass.async_add_executor_job(self.alarm_arm_night, code) + def alarm_arm_vacation(self, code: str | None = None) -> None: + """Send arm vacation command.""" + raise NotImplementedError() + + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: + """Send arm vacation command.""" + await self.hass.async_add_executor_job(self.alarm_arm_vacation, code) + def alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" raise NotImplementedError() diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index 36e3b6a13eb..f3688a27958 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -7,10 +7,12 @@ SUPPORT_ALARM_ARM_AWAY: Final = 2 SUPPORT_ALARM_ARM_NIGHT: Final = 4 SUPPORT_ALARM_TRIGGER: Final = 8 SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = 16 +SUPPORT_ALARM_ARM_VACATION: Final = 32 CONDITION_TRIGGERED: Final = "is_triggered" CONDITION_DISARMED: Final = "is_disarmed" CONDITION_ARMED_HOME: Final = "is_armed_home" CONDITION_ARMED_AWAY: Final = "is_armed_away" CONDITION_ARMED_NIGHT: Final = "is_armed_night" +CONDITION_ARMED_VACATION: Final = "is_armed_vacation" CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass" diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index d92f9615c9a..c37bddafcd3 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -16,6 +16,7 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) @@ -30,6 +31,7 @@ from .const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_TRIGGER, ) @@ -37,6 +39,7 @@ ACTION_TYPES: Final[set[str]] = { "arm_away", "arm_home", "arm_night", + "arm_vacation", "disarm", "trigger", } @@ -77,6 +80,8 @@ async def async_get_actions( actions.append({**base_action, CONF_TYPE: "arm_home"}) if supported_features & SUPPORT_ALARM_ARM_NIGHT: actions.append({**base_action, CONF_TYPE: "arm_night"}) + if supported_features & SUPPORT_ALARM_ARM_VACATION: + actions.append({**base_action, CONF_TYPE: "arm_vacation"}) actions.append({**base_action, CONF_TYPE: "disarm"}) if supported_features & SUPPORT_ALARM_TRIGGER: actions.append({**base_action, CONF_TYPE: "trigger"}) @@ -98,6 +103,8 @@ async def async_call_action_from_config( service = SERVICE_ALARM_ARM_HOME elif config[CONF_TYPE] == "arm_night": service = SERVICE_ALARM_ARM_NIGHT + elif config[CONF_TYPE] == "arm_vacation": + service = SERVICE_ALARM_ARM_VACATION elif config[CONF_TYPE] == "disarm": service = SERVICE_ALARM_DISARM elif config[CONF_TYPE] == "trigger": diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index 3cbaa019ad0..9367ef8f811 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -10,6 +10,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_CUSTOM_BYPASS, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -22,6 +23,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -37,6 +39,7 @@ from .const import ( CONDITION_ARMED_CUSTOM_BYPASS, CONDITION_ARMED_HOME, CONDITION_ARMED_NIGHT, + CONDITION_ARMED_VACATION, CONDITION_DISARMED, CONDITION_TRIGGERED, ) @@ -47,6 +50,7 @@ CONDITION_TYPES: Final[set[str]] = { CONDITION_ARMED_HOME, CONDITION_ARMED_AWAY, CONDITION_ARMED_NIGHT, + CONDITION_ARMED_VACATION, CONDITION_ARMED_CUSTOM_BYPASS, } @@ -90,6 +94,8 @@ async def async_get_conditions( conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY}) if supported_features & SUPPORT_ALARM_ARM_NIGHT: conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT}) + if supported_features & SUPPORT_ALARM_ARM_VACATION: + conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_VACATION}) if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS: conditions.append( {**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS} @@ -114,6 +120,8 @@ def async_condition_from_config( state = STATE_ALARM_ARMED_AWAY elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT: state = STATE_ALARM_ARMED_NIGHT + elif config[CONF_TYPE] == CONDITION_ARMED_VACATION: + state = STATE_ALARM_ARMED_VACATION elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS: state = STATE_ALARM_ARMED_CUSTOM_BYPASS diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index f89e03e7326..9ab6e466b6c 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -9,6 +9,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, ) from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA @@ -23,6 +24,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, @@ -39,6 +41,7 @@ TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | { "armed_home", "armed_away", "armed_night", + "armed_vacation", } TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -100,6 +103,13 @@ async def async_get_triggers( CONF_TYPE: "armed_night", } ) + if supported_features & SUPPORT_ALARM_ARM_VACATION: + triggers.append( + { + **base_trigger, + CONF_TYPE: "armed_vacation", + } + ) return triggers @@ -134,6 +144,8 @@ async def async_attach_trigger( to_state = STATE_ALARM_ARMED_AWAY elif config[CONF_TYPE] == "armed_night": to_state = STATE_ALARM_ARMED_NIGHT + elif config[CONF_TYPE] == "armed_vacation": + to_state = STATE_ALARM_ARMED_VACATION state_config = { state_trigger.CONF_PLATFORM: "state", diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py index 4bfb1486814..dabe49069d5 100644 --- a/homeassistant/components/alarm_control_panel/group.py +++ b/homeassistant/components/alarm_control_panel/group.py @@ -7,6 +7,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_TRIGGERED, STATE_OFF, ) @@ -24,6 +25,7 @@ def async_describe_on_off_states( STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_TRIGGERED, }, STATE_OFF, diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index 019ba35c013..3fcc540d04b 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -12,12 +12,14 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -32,6 +34,7 @@ VALID_STATES: Final[set[str]] = { STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, } @@ -71,6 +74,8 @@ async def _async_reproduce_state( service = SERVICE_ALARM_ARM_HOME elif state.state == STATE_ALARM_ARMED_NIGHT: service = SERVICE_ALARM_ARM_NIGHT + elif state.state == STATE_ALARM_ARMED_VACATION: + service = SERVICE_ALARM_ARM_VACATION elif state.state == STATE_ALARM_DISARMED: service = SERVICE_ALARM_DISARM elif state.state == STATE_ALARM_TRIGGERED: diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 8c148a6a1e0..0bf3952c4ed 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -70,6 +70,20 @@ alarm_arm_night: selector: text: +alarm_arm_vacation: + name: Arm vacation + description: Send the alarm the command for arm vacation. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to arm vacation the alarm control panel with. + example: "1234" + selector: + text: + alarm_trigger: name: Trigger description: Send the alarm the command for trigger. diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index de89d28082b..5126f49d92b 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -5,6 +5,7 @@ "arm_away": "Arm {entity_name} away", "arm_home": "Arm {entity_name} home", "arm_night": "Arm {entity_name} night", + "arm_vacation": "Arm {entity_name} vacation", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" }, @@ -13,14 +14,16 @@ "is_disarmed": "{entity_name} is disarmed", "is_armed_home": "{entity_name} is armed home", "is_armed_away": "{entity_name} is armed away", - "is_armed_night": "{entity_name} is armed night" + "is_armed_night": "{entity_name} is armed night", + "is_armed_vacation": "{entity_name} is armed vacation" }, "trigger_type": { "triggered": "{entity_name} triggered", "disarmed": "{entity_name} disarmed", "armed_home": "{entity_name} armed home", "armed_away": "{entity_name} armed away", - "armed_night": "{entity_name} armed night" + "armed_night": "{entity_name} armed night", + "armed_vacation": "{entity_name} armed vacation" } }, "state": { @@ -30,6 +33,7 @@ "armed_home": "Armed home", "armed_away": "Armed away", "armed_night": "Armed night", + "armed_vacation": "Armed vacation", "armed_custom_bypass": "Armed custom bypass", "pending": "Pending", "arming": "Arming", diff --git a/homeassistant/const.py b/homeassistant/const.py index 62f0e92738e..35b946fc6ab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -266,6 +266,7 @@ STATE_ALARM_DISARMED: Final = "disarmed" STATE_ALARM_ARMED_HOME: Final = "armed_home" STATE_ALARM_ARMED_AWAY: Final = "armed_away" STATE_ALARM_ARMED_NIGHT: Final = "armed_night" +STATE_ALARM_ARMED_VACATION: Final = "armed_vacation" STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = "armed_custom_bypass" STATE_ALARM_PENDING: Final = "pending" STATE_ALARM_ARMING: Final = "arming" @@ -580,6 +581,7 @@ SERVICE_ALARM_DISARM: Final = "alarm_disarm" SERVICE_ALARM_ARM_HOME: Final = "alarm_arm_home" SERVICE_ALARM_ARM_AWAY: Final = "alarm_arm_away" SERVICE_ALARM_ARM_NIGHT: Final = "alarm_arm_night" +SERVICE_ALARM_ARM_VACATION: Final = "alarm_arm_vacation" SERVICE_ALARM_ARM_CUSTOM_BYPASS: Final = "alarm_arm_custom_bypass" SERVICE_ALARM_TRIGGER: Final = "alarm_trigger" diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 1f66d901603..1fdb908d2e6 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, @@ -50,6 +51,7 @@ def entity_reg(hass): (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["disarm", "arm_away"]), (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["disarm", "arm_home"]), (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["disarm", "arm_night"]), + (True, 0, const.SUPPORT_ALARM_ARM_VACATION, ["disarm", "arm_vacation"]), (True, 0, const.SUPPORT_ALARM_TRIGGER, ["disarm", "trigger"]), ], ) @@ -150,13 +152,14 @@ async def test_get_action_capabilities( "arm_away": {"extra_fields": []}, "arm_home": {"extra_fields": []}, "arm_night": {"extra_fields": []}, + "arm_vacation": {"extra_fields": []}, "disarm": { "extra_fields": [{"name": "code", "optional": True, "type": "string"}] }, "trigger": {"extra_fields": []}, } actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 5 + assert len(actions) == 6 for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action @@ -196,13 +199,16 @@ async def test_get_action_capabilities_arm_code( "arm_night": { "extra_fields": [{"name": "code", "optional": True, "type": "string"}] }, + "arm_vacation": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, "disarm": { "extra_fields": [{"name": "code", "optional": True, "type": "string"}] }, "trigger": {"extra_fields": []}, } actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 5 + assert len(actions) == 6 for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action @@ -256,6 +262,18 @@ async def test_action(hass, enable_custom_integrations): "type": "arm_night", }, }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_vacation", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_vacation", + }, + }, { "trigger": {"platform": "event", "event_type": "test_event_disarm"}, "action": { @@ -302,6 +320,13 @@ async def test_action(hass, enable_custom_integrations): == STATE_ALARM_ARMED_HOME ) + hass.bus.async_fire("test_event_arm_vacation") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_VACATION + ) + hass.bus.async_fire("test_event_arm_night") await hass.async_block_till_done() assert ( diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index b1e2c171cea..d0644562850 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -50,11 +51,13 @@ def calls(hass): (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["is_armed_away"]), (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["is_armed_home"]), (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["is_armed_night"]), + (False, const.SUPPORT_ALARM_ARM_VACATION, 0, ["is_armed_vacation"]), (False, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, 0, ["is_armed_custom_bypass"]), (True, 0, 0, []), (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["is_armed_away"]), (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["is_armed_home"]), (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["is_armed_night"]), + (True, 0, const.SUPPORT_ALARM_ARM_VACATION, ["is_armed_vacation"]), (True, 0, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, ["is_armed_custom_bypass"]), ], ) @@ -212,6 +215,24 @@ async def test_if_state(hass, calls): }, { "trigger": {"platform": "event", "event_type": "test_event6"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_armed_vacation", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_armed_vacation - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event7"}, "condition": [ { "condition": "device", @@ -238,6 +259,7 @@ async def test_if_state(hass, calls): hass.bus.async_fire("test_event4") hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "is_triggered - event - test_event1" @@ -249,6 +271,7 @@ async def test_if_state(hass, calls): hass.bus.async_fire("test_event4") hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_disarmed - event - test_event2" @@ -260,6 +283,7 @@ async def test_if_state(hass, calls): hass.bus.async_fire("test_event4") hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") await hass.async_block_till_done() assert len(calls) == 3 assert calls[2].data["some"] == "is_armed_home - event - test_event3" @@ -271,6 +295,7 @@ async def test_if_state(hass, calls): hass.bus.async_fire("test_event4") hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") await hass.async_block_till_done() assert len(calls) == 4 assert calls[3].data["some"] == "is_armed_away - event - test_event4" @@ -282,10 +307,23 @@ async def test_if_state(hass, calls): hass.bus.async_fire("test_event4") hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") await hass.async_block_till_done() assert len(calls) == 5 assert calls[4].data["some"] == "is_armed_night - event - test_event5" + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_VACATION) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") + await hass.async_block_till_done() + assert len(calls) == 6 + assert calls[5].data["some"] == "is_armed_vacation - event - test_event6" + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_CUSTOM_BYPASS) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") @@ -293,6 +331,7 @@ async def test_if_state(hass, calls): hass.bus.async_fire("test_event4") hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") + hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 6 - assert calls[5].data["some"] == "is_armed_custom_bypass - event - test_event6" + assert len(calls) == 7 + assert calls[6].data["some"] == "is_armed_custom_bypass - event - test_event7" diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 8859915b911..9edda7e98e2 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -9,6 +9,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -54,7 +55,7 @@ def calls(hass): (False, 0, 0, ["triggered", "disarmed", "arming"]), ( False, - 15, + 47, 0, [ "triggered", @@ -63,13 +64,14 @@ def calls(hass): "armed_home", "armed_away", "armed_night", + "armed_vacation", ], ), (True, 0, 0, ["triggered", "disarmed", "arming"]), ( True, 0, - 15, + 47, [ "triggered", "disarmed", @@ -77,6 +79,7 @@ def calls(hass): "armed_home", "armed_away", "armed_night", + "armed_vacation", ], ), ], @@ -256,6 +259,25 @@ async def test_if_fires_on_state_change(hass, calls): }, }, }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_vacation", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_vacation - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, ] }, ) @@ -305,6 +327,15 @@ async def test_if_fires_on_state_change(hass, calls): == "armed_night - device - alarm_control_panel.entity - armed_away - armed_night - None" ) + # Fake that the entity is armed vacation. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_VACATION) + await hass.async_block_till_done() + assert len(calls) == 6 + assert ( + calls[5].data["some"] + == "armed_vacation - device - alarm_control_panel.entity - armed_night - armed_vacation - None" + ) + async def test_if_fires_on_state_change_with_for(hass, calls): """Test for triggers firing with delay.""" diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py index 686b281bff8..0f87e2206ac 100644 --- a/tests/components/alarm_control_panel/test_reproduce_state.py +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -4,12 +4,14 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -34,6 +36,9 @@ async def test_reproducing_states(hass, caplog): hass.states.async_set( "alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT, {} ) + hass.states.async_set( + "alarm_control_panel.entity_armed_vacation", STATE_ALARM_ARMED_VACATION, {} + ) hass.states.async_set( "alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED, {} ) @@ -53,6 +58,9 @@ async def test_reproducing_states(hass, caplog): arm_night_calls = async_mock_service( hass, "alarm_control_panel", SERVICE_ALARM_ARM_NIGHT ) + arm_vacation_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_VACATION + ) disarm_calls = async_mock_service(hass, "alarm_control_panel", SERVICE_ALARM_DISARM) trigger_calls = async_mock_service( hass, "alarm_control_panel", SERVICE_ALARM_TRIGGER @@ -68,6 +76,9 @@ async def test_reproducing_states(hass, caplog): ), State("alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME), State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT), + State( + "alarm_control_panel.entity_armed_vacation", STATE_ALARM_ARMED_VACATION + ), State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED), State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED), ] @@ -77,6 +88,7 @@ async def test_reproducing_states(hass, caplog): assert len(arm_custom_bypass_calls) == 0 assert len(arm_home_calls) == 0 assert len(arm_night_calls) == 0 + assert len(arm_vacation_calls) == 0 assert len(disarm_calls) == 0 assert len(trigger_calls) == 0 @@ -90,6 +102,7 @@ async def test_reproducing_states(hass, caplog): assert len(arm_custom_bypass_calls) == 0 assert len(arm_home_calls) == 0 assert len(arm_night_calls) == 0 + assert len(arm_vacation_calls) == 0 assert len(disarm_calls) == 0 assert len(trigger_calls) == 0 @@ -104,7 +117,8 @@ async def test_reproducing_states(hass, caplog): "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_CUSTOM_BYPASS ), State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_HOME), - State("alarm_control_panel.entity_disarmed", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_armed_vacation", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_ARMED_VACATION), State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED), # Should not raise State("alarm_control_panel.non_existing", "on"), @@ -132,6 +146,12 @@ async def test_reproducing_states(hass, caplog): assert len(arm_night_calls) == 1 assert arm_night_calls[0].domain == "alarm_control_panel" assert arm_night_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_vacation" + } + + assert len(arm_vacation_calls) == 1 + assert arm_vacation_calls[0].domain == "alarm_control_panel" + assert arm_vacation_calls[0].data == { "entity_id": "alarm_control_panel.entity_disarmed" } diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 7c16d067eff..160e6354b8b 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -185,12 +185,13 @@ async def test_websocket_get_action_capabilities( "alarm_control_panel", "test", "5678", device_id=device_entry.id ) hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + "alarm_control_panel.test_5678", "attributes", {"supported_features": 47} ) expected_capabilities = { "arm_away": {"extra_fields": []}, "arm_home": {"extra_fields": []}, "arm_night": {"extra_fields": []}, + "arm_vacation": {"extra_fields": []}, "disarm": { "extra_fields": [{"name": "code", "optional": True, "type": "string"}] }, @@ -209,7 +210,7 @@ async def test_websocket_get_action_capabilities( actions = msg["result"] id = 2 - assert len(actions) == 5 + assert len(actions) == 6 for action in actions: await client.send_json( { diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 864c99ec5df..f38bf48fc94 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -8,12 +8,14 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_TRIGGER, ) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -79,6 +81,7 @@ class MockAlarm(MockEntity, AlarmControlPanelEntity): | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_TRIGGER + | SUPPORT_ALARM_ARM_VACATION ) def alarm_arm_away(self, code=None): @@ -96,6 +99,11 @@ class MockAlarm(MockEntity, AlarmControlPanelEntity): self._state = STATE_ALARM_ARMED_NIGHT self.schedule_update_ha_state() + def alarm_arm_vacation(self, code=None): + """Send arm night command.""" + self._state = STATE_ALARM_ARMED_VACATION + self.schedule_update_ha_state() + def alarm_disarm(self, code=None): """Send disarm command.""" if code == "1234": From 520b500165448d059a6b524ead5509b1ae32bb0a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Jul 2021 17:34:59 +0200 Subject: [PATCH 022/818] Fix MQTT cover optimistic mode (#52392) * Fix MQTT cover optimistic mode * Add test --- homeassistant/components/mqtt/cover.py | 52 +++++++++++++++++++------- tests/components/mqtt/test_cover.py | 28 ++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index fd3e36c04e1..d920d12662f 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -245,11 +245,45 @@ class MqttCover(MqttEntity, CoverEntity): return PLATFORM_SCHEMA def _setup_from_config(self, config): - self._optimistic = config[CONF_OPTIMISTIC] or ( - config.get(CONF_STATE_TOPIC) is None + no_position = ( + config.get(CONF_SET_POSITION_TOPIC) is None and config.get(CONF_GET_POSITION_TOPIC) is None ) - self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC] + no_state = ( + config.get(CONF_COMMAND_TOPIC) is None + and config.get(CONF_STATE_TOPIC) is None + ) + no_tilt = ( + config.get(CONF_TILT_COMMAND_TOPIC) is None + and config.get(CONF_TILT_STATUS_TOPIC) is None + ) + optimistic_position = ( + config.get(CONF_SET_POSITION_TOPIC) is not None + and config.get(CONF_GET_POSITION_TOPIC) is None + ) + optimistic_state = ( + config.get(CONF_COMMAND_TOPIC) is not None + and config.get(CONF_STATE_TOPIC) is None + ) + optimistic_tilt = ( + config.get(CONF_TILT_COMMAND_TOPIC) is not None + and config.get(CONF_TILT_STATUS_TOPIC) is None + ) + + if config[CONF_OPTIMISTIC] or ( + (no_position or optimistic_position) + and (no_state or optimistic_state) + and (no_tilt or optimistic_tilt) + ): + # Force into optimistic mode. + self._optimistic = True + + if ( + config[CONF_TILT_STATE_OPTIMISTIC] + or config.get(CONF_TILT_STATUS_TOPIC) is None + ): + # Force into optimistic tilt mode. + self._tilt_optimistic = True value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: @@ -418,17 +452,7 @@ class MqttCover(MqttEntity, CoverEntity): "qos": self._config[CONF_QOS], } - if ( - self._config.get(CONF_GET_POSITION_TOPIC) is None - and self._config.get(CONF_STATE_TOPIC) is None - ): - # Force into optimistic mode. - self._optimistic = True - - if self._config.get(CONF_TILT_STATUS_TOPIC) is None: - # Force into optimistic tilt mode. - self._tilt_optimistic = True - else: + if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: self._tilt_value = STATE_UNKNOWN topics["tilt_status_topic"] = { "topic": self._config.get(CONF_TILT_STATUS_TOPIC), diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 7d763895428..0ee2557fbd6 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -393,6 +393,34 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): assert current_cover_position == 20 +@pytest.mark.parametrize( + "config, assumed_state", + [ + ({"command_topic": "abc"}, True), + ({"command_topic": "abc", "state_topic": "abc"}, False), + # ({"set_position_topic": "abc"}, True), - not a valid configuration + ({"set_position_topic": "abc", "position_topic": "abc"}, False), + ({"tilt_command_topic": "abc"}, True), + ({"tilt_command_topic": "abc", "tilt_status_topic": "abc"}, False), + ], +) +async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): + """Test assumed_state is set correctly.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + if assumed_state: + assert ATTR_ASSUMED_STATE in state.attributes + else: + assert ATTR_ASSUMED_STATE not in state.attributes + + async def test_optimistic_state_change(hass, mqtt_mock): """Test changing state optimistically.""" assert await async_setup_component( From 8e846164a4ad1c69272c3438a92cd83b2abca0d1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 1 Jul 2021 14:05:55 -0400 Subject: [PATCH 023/818] Bump up ZHA dependencies (#52374) * Bump up ZHA dependencies * Fix broken tests * Update tests/components/zha/common.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/common.py | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d1e79d1b67b..b366b73d6c8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,10 +7,10 @@ "bellows==0.25.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.57", + "zha-quirks==0.0.58", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.34.0", + "zigpy==0.35.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.1" diff --git a/requirements_all.txt b/requirements_all.txt index 9ed9b4181a6..0fe1fd626c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2431,7 +2431,7 @@ zengge==0.2 zeroconf==0.32.0 # homeassistant.components.zha -zha-quirks==0.0.57 +zha-quirks==0.0.58 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2455,7 +2455,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.34.0 +zigpy==0.35.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf8d9fd8138..df8fd6591e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,7 +1334,7 @@ zeep[async]==4.0.0 zeroconf==0.32.0 # homeassistant.components.zha -zha-quirks==0.0.57 +zha-quirks==0.0.58 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1352,7 +1352,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.34.0 +zigpy==0.35.0 # homeassistant.components.zwave_js zwave-js-server-python==0.27.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 45caed95ae6..eb65cc4fd2e 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -40,8 +40,9 @@ class FakeEndpoint: if _patch_cluster: patch_cluster(cluster) self.in_clusters[cluster_id] = cluster - if hasattr(cluster, "ep_attribute"): - setattr(self, cluster.ep_attribute, cluster) + ep_attribute = cluster.ep_attribute + if ep_attribute: + setattr(self, ep_attribute, cluster) def add_output_cluster(self, cluster_id, _patch_cluster=True): """Add an output cluster.""" From 1337dfed8c722294bd10edb48a338b9d8c6d1e90 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Thu, 1 Jul 2021 22:31:30 +0300 Subject: [PATCH 024/818] Use attributes instead of properties for uptime (#52398) * Use attributes instead of properties for uptime * add missing types --- homeassistant/components/uptime/sensor.py | 26 ++++------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 98c673b8878..5b31b2e81d0 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -47,25 +47,7 @@ class UptimeSensor(SensorEntity): def __init__(self, name: str) -> None: """Initialize the uptime sensor.""" - self._name = name - self._state = dt_util.now().isoformat() - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def device_class(self) -> str: - """Return device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def state(self) -> str: - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self) -> bool: - """Disable polling for this entity.""" - return False + self._attr_name: str = name + self._attr_device_class: str = DEVICE_CLASS_TIMESTAMP + self._attr_should_poll: bool = False + self._attr_state: str = dt_util.now().isoformat() From d6ed5dac8b257f325c5793cf7d56c9eef2e2781b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Thu, 1 Jul 2021 21:32:46 +0200 Subject: [PATCH 025/818] Bump pysma to 0.6.1 (#52401) --- homeassistant/components/sma/__init__.py | 2 +- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 0df3ef8cb7c..2eb0e6760ed 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -167,7 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: pysma.exceptions.SmaReadException, pysma.exceptions.SmaConnectionException, ) as exc: - raise UpdateFailed from exc + raise UpdateFailed(exc) from exc interval = timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 721431b89a7..85f6de7cb7c 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.0"], + "requirements": ["pysma==0.6.1"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0fe1fd626c5..75ec661b037 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1756,7 +1756,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.0 +pysma==0.6.1 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df8fd6591e8..501a0199935 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.0 +pysma==0.6.1 # homeassistant.components.smappee pysmappee==0.2.25 From a85ba1b34cef0bc1bf6c9aa0f5b95dcfb7e5c47f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 00:30:33 +0200 Subject: [PATCH 026/818] Upgrade wled to 0.7.1 (#52405) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 237f6850b66..348109f6b87 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.7.0"], + "requirements": ["wled==0.7.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 75ec661b037..5ae7791e591 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ wirelesstagpy==0.4.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.0 +wled==0.7.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 501a0199935..51483cff9f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1302,7 +1302,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.0 +wled==0.7.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 From ad20b5ced020211acbf94c58abce5df16d927fd1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 2 Jul 2021 00:11:25 +0000 Subject: [PATCH 027/818] [ci skip] Translation update --- .../alarm_control_panel/translations/en.json | 4 ++++ .../alarm_control_panel/translations/et.json | 4 ++++ .../alarm_control_panel/translations/pl.json | 4 ++++ .../alarm_control_panel/translations/ru.json | 4 ++++ .../translations/zh-Hant.json | 4 ++++ .../freedompro/translations/no.json | 20 +++++++++++++++++++ .../components/motioneye/translations/ca.json | 10 ++++++++++ .../components/motioneye/translations/de.json | 10 ++++++++++ .../components/motioneye/translations/en.json | 10 ++++++++++ .../components/motioneye/translations/et.json | 10 ++++++++++ .../components/motioneye/translations/no.json | 10 ++++++++++ .../components/motioneye/translations/pl.json | 10 ++++++++++ .../components/motioneye/translations/ru.json | 10 ++++++++++ .../motioneye/translations/zh-Hant.json | 10 ++++++++++ .../xiaomi_miio/translations/de.json | 6 +++--- .../yamaha_musiccast/translations/de.json | 6 +++--- 16 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/freedompro/translations/no.json diff --git a/homeassistant/components/alarm_control_panel/translations/en.json b/homeassistant/components/alarm_control_panel/translations/en.json index b364d850461..c9e9541fc30 100644 --- a/homeassistant/components/alarm_control_panel/translations/en.json +++ b/homeassistant/components/alarm_control_panel/translations/en.json @@ -4,6 +4,7 @@ "arm_away": "Arm {entity_name} away", "arm_home": "Arm {entity_name} home", "arm_night": "Arm {entity_name} night", + "arm_vacation": "Arm {entity_name} vacation", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} is armed away", "is_armed_home": "{entity_name} is armed home", "is_armed_night": "{entity_name} is armed night", + "is_armed_vacation": "{entity_name} is armed vacation", "is_disarmed": "{entity_name} is disarmed", "is_triggered": "{entity_name} is triggered" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} armed away", "armed_home": "{entity_name} armed home", "armed_night": "{entity_name} armed night", + "armed_vacation": "{entity_name} armed vacation", "disarmed": "{entity_name} disarmed", "triggered": "{entity_name} triggered" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Armed custom bypass", "armed_home": "Armed home", "armed_night": "Armed night", + "armed_vacation": "Armed vacation", "arming": "Arming", "disarmed": "Disarmed", "disarming": "Disarming", diff --git a/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant/components/alarm_control_panel/translations/et.json index cc4bb6f1ea3..1c3ddf0e1b2 100644 --- a/homeassistant/components/alarm_control_panel/translations/et.json +++ b/homeassistant/components/alarm_control_panel/translations/et.json @@ -4,6 +4,7 @@ "arm_away": "Valvesta {entity_name}", "arm_home": "Valvesta {entity_name} kodus re\u017eiimis", "arm_night": "Valvesta {entity_name} \u00f6\u00f6re\u017eiimis", + "arm_vacation": "Valvesta {entity_name} puhkusere\u017eiimis", "disarm": "V\u00f5ta {entity_name} valvest maha", "trigger": "K\u00e4ivita {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} on valvestatud", "is_armed_home": "{entity_name} on valvestatud kodure\u017eiimis", "is_armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis", + "is_armed_vacation": "{entity_name} on valvestatud puhkuse reziimis", "is_disarmed": "{entity_name} on valve alt maas", "is_triggered": "{entity_name} on h\u00e4iret andnud" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} valvestati", "armed_home": "{entity_name} valvestati kodure\u017eiimis", "armed_night": "{entity_name} valvestati \u00f6\u00f6re\u017eiimis", + "armed_vacation": "{entity_name} puhkuse re\u017eiim", "disarmed": "{entity_name} v\u00f5eti valvest maha", "triggered": "{entity_name} andis h\u00e4iret" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Valves, eranditega", "armed_home": "Valves kodus", "armed_night": "Valves \u00f6ine", + "armed_vacation": "Valvestatud puhkuse re\u017eiimis", "arming": "Valvestab", "disarmed": "Maas", "disarming": "Maas...", diff --git a/homeassistant/components/alarm_control_panel/translations/pl.json b/homeassistant/components/alarm_control_panel/translations/pl.json index 0fd3045d1df..b65dbd59282 100644 --- a/homeassistant/components/alarm_control_panel/translations/pl.json +++ b/homeassistant/components/alarm_control_panel/translations/pl.json @@ -4,6 +4,7 @@ "arm_away": "uzbr\u00f3j (poza domem) {entity_name}", "arm_home": "uzbr\u00f3j (w domu) {entity_name}", "arm_night": "uzbr\u00f3j (noc) {entity_name}", + "arm_vacation": "uzbr\u00f3j (tryb wakacyjny) {entity_name}", "disarm": "rozbr\u00f3j {entity_name}", "trigger": "wyzw\u00f3l {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "alarm {entity_name} jest uzbrojony (poza domem)", "is_armed_home": "alarm {entity_name} jest uzbrojony (w domu)", "is_armed_night": "alarm {entity_name} jest uzbrojony (noc)", + "is_armed_vacation": "alarm {entity_name} jest uzbrojony (tryb wakacyjny)", "is_disarmed": "alarm {entity_name} jest rozbrojony", "is_triggered": "alarm {entity_name} jest wyzwolony" }, @@ -18,6 +20,7 @@ "armed_away": "alarm {entity_name} zostanie uzbrojony (poza domem)", "armed_home": "alarm {entity_name} zostanie uzbrojony (w domu)", "armed_night": "alarm {entity_name} zostanie uzbrojony (noc)", + "armed_vacation": "alarm {entity_name} zostanie uzbrojony (tryb wakacyjny)", "disarmed": "alarm {entity_name} zostanie rozbrojony", "triggered": "alarm {entity_name} zostanie wyzwolony" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "uzbrojony (cz\u0119\u015bciowo)", "armed_home": "uzbrojony (w domu)", "armed_night": "uzbrojony (noc)", + "armed_vacation": "uzbrojony (tryb wakacyjny)", "arming": "uzbrajanie", "disarmed": "rozbrojony", "disarming": "rozbrajanie", diff --git a/homeassistant/components/alarm_control_panel/translations/ru.json b/homeassistant/components/alarm_control_panel/translations/ru.json index f390f017328..61e46b3db1f 100644 --- a/homeassistant/components/alarm_control_panel/translations/ru.json +++ b/homeassistant/components/alarm_control_panel/translations/ru.json @@ -4,6 +4,7 @@ "arm_away": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "arm_home": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_vacation": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041e\u0442\u043f\u0443\u0441\u043a\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" }, @@ -11,6 +12,7 @@ "is_armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "is_armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "is_armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_armed_vacation": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041e\u0442\u043f\u0443\u0441\u043a\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "is_disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "is_triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" }, @@ -18,6 +20,7 @@ "armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_vacation": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041e\u0442\u043f\u0443\u0441\u043a\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "\u041e\u0445\u0440\u0430\u043d\u0430 \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438", "armed_home": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u0434\u043e\u043c\u0430)", "armed_night": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u043d\u043e\u0447\u044c)", + "armed_vacation": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u043e\u0442\u043f\u0443\u0441\u043a)", "arming": "\u041f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", "disarmed": "\u0421\u043d\u044f\u0442\u043e \u0441 \u043e\u0445\u0440\u0430\u043d\u044b", "disarming": "\u0421\u043d\u044f\u0442\u0438\u0435 \u0441 \u043e\u0445\u0440\u0430\u043d\u044b", diff --git a/homeassistant/components/alarm_control_panel/translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/translations/zh-Hant.json index 2dac00f9990..74d046c233d 100644 --- a/homeassistant/components/alarm_control_panel/translations/zh-Hant.json +++ b/homeassistant/components/alarm_control_panel/translations/zh-Hant.json @@ -4,6 +4,7 @@ "arm_away": "\u8a2d\u5b9a{entity_name}\u5916\u51fa\u6a21\u5f0f", "arm_home": "\u8a2d\u5b9a{entity_name}\u8fd4\u5bb6\u6a21\u5f0f", "arm_night": "\u8a2d\u5b9a{entity_name}\u591c\u9593\u6a21\u5f0f", + "arm_vacation": "\u8a2d\u5b9a{entity_name}\u5ea6\u5047\u6a21\u5f0f", "disarm": "\u89e3\u9664{entity_name}", "trigger": "\u89f8\u767c{entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", "is_armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", "is_armed_night": "{entity_name}\u8a2d\u5b9a\u591c\u9593", + "is_armed_vacation": "{entity_name}\u8a2d\u5b9a\u5ea6\u5047", "is_disarmed": "{entity_name}\u5df2\u89e3\u9664", "is_triggered": "{entity_name}\u5df2\u89f8\u767c" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", "armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", "armed_night": "{entity_name}\u8a2d\u5b9a\u591c\u9593", + "armed_vacation": "{entity_name}\u8a2d\u5b9a\u5ea6\u5047", "disarmed": "{entity_name}\u5df2\u89e3\u9664", "triggered": "{entity_name}\u5df2\u89f8\u767c" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "\u8b66\u6212\u6a21\u5f0f\u72c0\u614b", "armed_home": "\u5728\u5bb6\u8b66\u6212", "armed_night": "\u591c\u9593\u8b66\u6212", + "armed_vacation": "\u5ea6\u5047\u8b66\u6212", "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u8b66\u6212\u89e3\u9664", "disarming": "\u89e3\u9664\u4e2d", diff --git a/homeassistant/components/freedompro/translations/no.json b/homeassistant/components/freedompro/translations/no.json new file mode 100644 index 00000000000..39a3e339d9a --- /dev/null +++ b/homeassistant/components/freedompro/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn API-n\u00f8kkelen hentet fra https://home.freedompro.eu", + "title": "Freedompro API-n\u00f8kkel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json index 8f11dba2802..85477627f38 100644 --- a/homeassistant/components/motioneye/translations/ca.json +++ b/homeassistant/components/motioneye/translations/ca.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Configura els webhooks de motionEye per enviar esdeveniments a Home Assistant", + "webhook_set_overwrite": "Sobreescriu els webhooks no reconeguts" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index 3370717366d..0a0ca806555 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "MotionEye-Webhooks konfigurieren, um Ereignisse an Home Assistant zu melden", + "webhook_set_overwrite": "\u00dcberschreiben von nicht bekannten Webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json index b93e4f66894..6c24b7850d4 100644 --- a/homeassistant/components/motioneye/translations/en.json +++ b/homeassistant/components/motioneye/translations/en.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Configure motionEye webhooks to report events to Home Assistant", + "webhook_set_overwrite": "Overwrite unrecognized webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json index 1b0861dbc75..b3e3919123c 100644 --- a/homeassistant/components/motioneye/translations/et.json +++ b/homeassistant/components/motioneye/translations/et.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Seadista motionEye veebihaagid, et teatada s\u00fcndmustest Home Assistanti'le", + "webhook_set_overwrite": "Kirjuta tundmatud veebihaagid \u00fcle" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json index 33dadffa94f..c0fd5e881c2 100644 --- a/homeassistant/components/motioneye/translations/no.json +++ b/homeassistant/components/motioneye/translations/no.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Konfigurer motionEye webhooks for \u00e5 rapportere hendelser til Home Assistant", + "webhook_set_overwrite": "Overskriv ukjente webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json index 296c2f963af..7267a70b677 100644 --- a/homeassistant/components/motioneye/translations/pl.json +++ b/homeassistant/components/motioneye/translations/pl.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Skonfiguruj webhook motionEye, aby zg\u0142asza\u0107 zdarzenia do Home Assistanta", + "webhook_set_overwrite": "Nadpisz nierozpoznane webhooki" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json index 8999b0e8f82..fbda6e7abdc 100644 --- a/homeassistant/components/motioneye/translations/ru.json +++ b/homeassistant/components/motioneye/translations/ru.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Webhook motionEye \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0445 \u0432 Home Assistant", + "webhook_set_overwrite": "\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043d\u044b\u0435 Webhook" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json index 8c143655f2f..a443ee6954b 100644 --- a/homeassistant/components/motioneye/translations/zh-Hant.json +++ b/homeassistant/components/motioneye/translations/zh-Hant.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "\u8a2d\u5b9a motionEye webhooks \u4ee5\u56de\u5831\u4e8b\u4ef6\u81f3 Home Assistant", + "webhook_set_overwrite": "\u8986\u84cb\u7121\u6cd5\u8fa8\u8b58\u7684 Webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 7f541180a55..13f20fe29b2 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -5,7 +5,7 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "incomplete_info": "Unvollst\u00e4ndige Informationen zur Einrichtung des Ger\u00e4ts, kein Host oder Token geliefert.", "not_xiaomi_miio": "Ger\u00e4t wird (noch) nicht von Xiaomi Miio unterst\u00fctzt.", - "reauth_successful": "[%key::common::config_flow::abort::reauth_successful%]" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -55,7 +55,7 @@ }, "manual": { "data": { - "host": "[%key::common::config_flow::data::ip%]", + "host": "IP-Adresse", "token": "API-Token" }, "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr Anweisungen. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara-Integration verwendet wird.", @@ -63,7 +63,7 @@ }, "reauth_confirm": { "description": "Die Xiaomi Miio-Integration muss dein Konto neu authentifizieren, um die Token zu aktualisieren oder fehlende Cloud-Anmeldedaten hinzuzuf\u00fcgen.", - "title": "[%key::common::config_flow::title::reauth%]" + "title": "Integration erneut authentifizieren" }, "select": { "data": { diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json index 49e66419bdf..28022aec025 100644 --- a/homeassistant/components/yamaha_musiccast/translations/de.json +++ b/homeassistant/components/yamaha_musiccast/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key::common::config_flow::abort::already_configured_device%]", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "yxc_control_url_missing": "Die Steuer-URL ist in der ssdp-Beschreibung nicht angegeben." }, "error": { @@ -10,11 +10,11 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "[%key::common::config_flow::description::confirm_setup%]" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" }, "user": { "data": { - "host": "[%key::common::config_flow::data::host%]" + "host": "Host" }, "description": "Einrichten von MusicCast zur Integration mit Home Assistant." } From 9d04ba280470d945c24faf1a19384e85b96073ae Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Jul 2021 01:36:48 -0400 Subject: [PATCH 028/818] Bump eight sleep dependency to fix bug (#52408) --- .../components/eight_sleep/__init__.py | 27 ++++++++----------- .../components/eight_sleep/binary_sensor.py | 14 +++++----- .../components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 67c195da3e6..4e16cd1087f 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -11,10 +11,10 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SENSORS, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -29,7 +29,6 @@ _LOGGER = logging.getLogger(__name__) CONF_PARTNER = "partner" DATA_EIGHT = "eight_sleep" -DEFAULT_PARTNER = False DOMAIN = "eight_sleep" HEAT_ENTITY = "heat" @@ -86,12 +85,15 @@ SERVICE_EIGHT_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean, - } + DOMAIN: vol.All( + cv.deprecated(CONF_PARTNER), + vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PARTNER): cv.boolean, + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -104,7 +106,6 @@ async def async_setup(hass, config): conf = config.get(DOMAIN) user = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) - partner = conf.get(CONF_PARTNER) if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant") @@ -112,7 +113,7 @@ async def async_setup(hass, config): timezone = str(hass.config.time_zone) - eight = EightSleep(user, password, timezone, partner, None, hass.loop) + eight = EightSleep(user, password, timezone, async_get_clientsession(hass)) hass.data[DATA_EIGHT] = eight @@ -190,12 +191,6 @@ async def async_setup(hass, config): DOMAIN, SERVICE_HEAT_SET, async_service_handler, schema=SERVICE_EIGHT_SCHEMA ) - async def stop_eight(event): - """Handle stopping eight api session.""" - await eight.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight) - return True diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 803b20383b6..d8a763c2e54 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Eight Sleep binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + BinarySensorEntity, +) from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity @@ -34,13 +37,15 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" self._state = None self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) self._usrobj = self._eight.users[self._userid] + self._attr_name = f"{name} {self._mapped_name}" + self._attr_device_class = DEVICE_CLASS_OCCUPANCY + _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", self._sensor, @@ -48,11 +53,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): self._userid, ) - @property - def name(self): - """Return the name of the sensor, if any.""" - return self._name - @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index d0f86d5a5e4..fb3762cf738 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.1.5"], + "requirements": ["pyeight==0.1.8"], "codeowners": ["@mezz64"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5ae7791e591..3a7808389df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1409,7 +1409,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.eight_sleep -pyeight==0.1.5 +pyeight==0.1.8 # homeassistant.components.emby pyemby==1.7 From 6fa312d476af00ff92776dbc655aed992d6fc9ca Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 2 Jul 2021 00:49:05 -0500 Subject: [PATCH 029/818] Remove redundant property definitions in Guardian (#52361) * Remove redundant property definitions in Guardian * Update incorrect attributes --- homeassistant/components/guardian/__init__.py | 106 ++++++------------ .../components/guardian/binary_sensor.py | 48 +++----- homeassistant/components/guardian/sensor.py | 56 ++------- homeassistant/components/guardian/switch.py | 21 +--- 4 files changed, 67 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 89e038b047e..0b2d7c634c5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -9,7 +9,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -218,34 +217,12 @@ class GuardianEntity(CoordinatorEntity): self, entry: ConfigEntry, kind: str, name: str, device_class: str, icon: str ) -> None: """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: "Data provided by Elexa"} - self._available = True + self._attr_device_class = device_class + self._attr_device_info = {"manufacturer": "Elexa"} + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: "Data provided by Elexa"} + self._attr_icon = icon + self._attr_name = name self._entry = entry - self._device_class = device_class - self._device_info = {"manufacturer": "Elexa"} - self._icon = icon - self._kind = kind - self._name = name - - @property - def device_class(self) -> str: - """Return the device class.""" - return self._device_class - - @property - def device_info(self) -> DeviceInfo: - """Return device registry information for this entity.""" - return self._device_info - - @property - def extra_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon @callback def _async_update_from_latest_data(self): @@ -255,12 +232,6 @@ class GuardianEntity(CoordinatorEntity): """ raise NotImplementedError - @callback - def _async_update_state_callback(self): - """Update the entity's state.""" - self._async_update_from_latest_data() - self.async_write_ha_state() - class PairedSensorEntity(GuardianEntity): """Define a Guardian paired sensor entity.""" @@ -277,24 +248,15 @@ class PairedSensorEntity(GuardianEntity): """Initialize.""" super().__init__(entry, kind, name, device_class, icon) + paired_sensor_uid = coordinator.data["uid"] + self._attr_device_info["identifiers"] = {(DOMAIN, paired_sensor_uid)} + self._attr_device_info["name"] = f"Guardian Paired Sensor {paired_sensor_uid}" + self._attr_device_info["via_device"] = (DOMAIN, entry.data[CONF_UID]) + self._attr_name = f"Guardian Paired Sensor {paired_sensor_uid}: {name}" + self._attr_unique_id = f"{paired_sensor_uid}_{kind}" + self._kind = kind self.coordinator = coordinator - self._paired_sensor_uid = coordinator.data["uid"] - - self._device_info["identifiers"] = {(DOMAIN, self._paired_sensor_uid)} - self._device_info["name"] = f"Guardian Paired Sensor {self._paired_sensor_uid}" - self._device_info["via_device"] = (DOMAIN, self._entry.data[CONF_UID]) - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"Guardian Paired Sensor {self._paired_sensor_uid}: {self._name}" - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._paired_sensor_uid}_{self._kind}" - async def async_added_to_hass(self) -> None: """Perform tasks when the entity is added.""" self._async_update_from_latest_data() @@ -315,30 +277,26 @@ class ValveControllerEntity(GuardianEntity): """Initialize.""" super().__init__(entry, kind, name, device_class, icon) - self.coordinators = coordinators - - self._device_info["identifiers"] = {(DOMAIN, self._entry.data[CONF_UID])} - self._device_info[ + self._attr_device_info["identifiers"] = {(DOMAIN, entry.data[CONF_UID])} + self._attr_device_info[ "name" - ] = f"Guardian Valve Controller {self._entry.data[CONF_UID]}" - self._device_info["model"] = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[ + ] = f"Guardian Valve Controller {entry.data[CONF_UID]}" + self._attr_device_info["model"] = coordinators[API_SYSTEM_DIAGNOSTICS].data[ "firmware" ] + self._attr_name = f"Guardian {entry.data[CONF_UID]}: {name}" + self._attr_unique_id = f"{entry.data[CONF_UID]}_{kind}" + self._kind = kind + self.coordinators = coordinators @property - def availabile(self) -> bool: + def available(self) -> bool: """Return if entity is available.""" - return any(coordinator.last_update_success for coordinator in self.coordinators) - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"Guardian {self._entry.data[CONF_UID]}: {self._name}" - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._entry.data[CONF_UID]}_{self._kind}" + return any( + coordinator.last_update_success + for coordinator in self.coordinators.values() + if coordinator + ) async def _async_continue_entity_setup(self): """Perform additional, internal tasks when the entity is about to be added. @@ -350,9 +308,14 @@ class ValveControllerEntity(GuardianEntity): @callback def async_add_coordinator_update_listener(self, api: str) -> None: """Add a listener to a DataUpdateCoordinator based on the API referenced.""" - self.async_on_remove( - self.coordinators[api].async_add_listener(self._async_update_state_callback) - ) + + @callback + def update(): + """Update the entity's state.""" + self._async_update_from_latest_data() + self.async_write_ha_state() + + self.async_on_remove(self.coordinators[api].async_add_listener(update)) async def async_added_to_hass(self) -> None: """Perform tasks when the entity is added.""" @@ -365,7 +328,6 @@ class ValveControllerEntity(GuardianEntity): Only used by the generic entity update service. """ - # Ignore manual update requests if the entity is disabled if not self.enabled: return diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 869acc094d5..7d38a431f4c 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -129,25 +129,15 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): """Initialize.""" super().__init__(entry, coordinator, kind, name, device_class, icon) - self._is_on = True - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - return self.coordinator.last_update_success - - @property - def is_on(self) -> bool: - """Return True if the binary sensor is on.""" - return self._is_on + self._attr_is_on = True @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_LEAK_DETECTED: - self._is_on = self.coordinator.data["wet"] + self._attr_is_on = self.coordinator.data["wet"] elif self._kind == SENSOR_KIND_MOVED: - self._is_on = self.coordinator.data["moved"] + self._attr_is_on = self.coordinator.data["moved"] class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): @@ -165,23 +155,7 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): """Initialize.""" super().__init__(entry, coordinators, kind, name, device_class, icon) - self._is_on = True - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - if self._kind == SENSOR_KIND_AP_INFO: - return self.coordinators[API_WIFI_STATUS].last_update_success - if self._kind == SENSOR_KIND_LEAK_DETECTED: - return self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - return False - - @property - def is_on(self) -> bool: - """Return True if the binary sensor is on.""" - return self._is_on + self._attr_is_on = True async def _async_continue_entity_setup(self) -> None: """Add an API listener.""" @@ -194,8 +168,13 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_AP_INFO: - self._is_on = self.coordinators[API_WIFI_STATUS].data["station_connected"] - self._attrs.update( + self._attr_available = self.coordinators[ + API_WIFI_STATUS + ].last_update_success + self._attr_is_on = self.coordinators[API_WIFI_STATUS].data[ + "station_connected" + ] + self._attr_extra_state_attributes.update( { ATTR_CONNECTED_CLIENTS: self.coordinators[API_WIFI_STATUS].data.get( "ap_clients" @@ -203,6 +182,9 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): } ) elif self._kind == SENSOR_KIND_LEAK_DETECTED: - self._is_on = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ + self._attr_available = self.coordinators[ + API_SYSTEM_ONBOARD_SENSOR_STATUS + ].last_update_success + self._attr_is_on = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ "wet" ] diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 2d62fe2c613..1aaf83b8fb8 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -126,31 +126,15 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Initialize.""" super().__init__(entry, coordinator, kind, name, device_class, icon) - self._state = None - self._unit = unit - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - return self.coordinator.last_update_success - - @property - def state(self) -> str: - """Return the sensor state.""" - return self._state - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return self._unit + self._attr_unit_of_measurement = unit @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_BATTERY: - self._state = self.coordinator.data["battery"] + self._attr_state = self.coordinator.data["battery"] elif self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self.coordinator.data["temperature"] + self._attr_state = self.coordinator.data["temperature"] class ValveControllerSensor(ValveControllerEntity, SensorEntity): @@ -169,29 +153,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Initialize.""" super().__init__(entry, coordinators, kind, name, device_class, icon) - self._state = None - self._unit = unit - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - if self._kind == SENSOR_KIND_TEMPERATURE: - return self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - if self._kind == SENSOR_KIND_UPTIME: - return self.coordinators[API_SYSTEM_DIAGNOSTICS].last_update_success - return False - - @property - def state(self) -> str: - """Return the sensor state.""" - return self._state - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return self._unit + self._attr_unit_of_measurement = unit async def _async_continue_entity_setup(self) -> None: """Register API interest (and related tasks) when the entity is added.""" @@ -202,8 +164,14 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ + self._attr_available = self.coordinators[ + API_SYSTEM_ONBOARD_SENSOR_STATUS + ].last_update_success + self._attr_state = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ "temperature" ] elif self._kind == SENSOR_KIND_UPTIME: - self._state = self.coordinators[API_SYSTEM_DIAGNOSTICS].data["uptime"] + self._attr_available = self.coordinators[ + API_SYSTEM_DIAGNOSTICS + ].last_update_success + self._attr_state = self.coordinators[API_SYSTEM_DIAGNOSTICS].data["uptime"] diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index fe39ee635f4..29069c1abc5 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -92,18 +92,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): entry, coordinators, "valve", "Valve Controller", None, "mdi:water" ) + self._attr_is_on = True self._client = client - self._is_on = True - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - return self.coordinators[API_VALVE_STATUS].last_update_success - - @property - def is_on(self) -> bool: - """Return True if the valve is open.""" - return self._is_on async def _async_continue_entity_setup(self): """Register API interest (and related tasks) when the entity is added.""" @@ -112,14 +102,15 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._is_on = self.coordinators[API_VALVE_STATUS].data["state"] in ( + self._attr_available = self.coordinators[API_VALVE_STATUS].last_update_success + self._attr_is_on = self.coordinators[API_VALVE_STATUS].data["state"] in ( "start_opening", "opening", "finish_opening", "opened", ) - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_AVG_CURRENT: self.coordinators[API_VALVE_STATUS].data[ "average_current" @@ -215,7 +206,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): LOGGER.error("Error while closing the valve: %s", err) return - self._is_on = False + self._attr_is_on = False self.async_write_ha_state() async def async_turn_on(self, **kwargs) -> None: @@ -227,5 +218,5 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): LOGGER.error("Error while opening the valve: %s", err) return - self._is_on = True + self._attr_is_on = True self.async_write_ha_state() From 887753e06da188b7b7c436a37881170671d36b87 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 2 Jul 2021 02:47:49 -0500 Subject: [PATCH 030/818] Remove redundant property definitions in OpenUV (#52379) * Remove redundant property definitions in OpenUV * Update homeassistant/components/openuv/__init__.py Co-authored-by: Franck Nijhof * Code review Co-authored-by: Franck Nijhof --- homeassistant/components/openuv/__init__.py | 26 ++----- .../components/openuv/binary_sensor.py | 46 +++---------- homeassistant/components/openuv/sensor.py | 67 +++++-------------- 3 files changed, 34 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index e1af166a3c2..72bdbbd179a 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -166,28 +166,16 @@ class OpenUV: class OpenUvEntity(Entity): """Define a generic OpenUV entity.""" - def __init__(self, openuv): + def __init__(self, openuv, sensor_type): """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._available = True - self._name = None + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_should_poll = False + self._attr_unique_id = ( + f"{openuv.client.latitude}_{openuv.client.longitude}_{sensor_type}" + ) + self._sensor_type = sensor_type self.openuv = openuv - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def name(self): - """Return the name of the entity.""" - return self._name - async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 62a83cdb141..eac67909b86 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -27,9 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities): binary_sensors = [] for kind, attrs in BINARY_SENSORS.items(): name, icon = attrs - binary_sensors.append( - OpenUvBinarySensor(openuv, kind, name, icon, entry.entry_id) - ) + binary_sensors.append(OpenUvBinarySensor(openuv, kind, name, icon)) async_add_entities(binary_sensors, True) @@ -37,38 +35,12 @@ async def async_setup_entry(hass, entry, async_add_entities): class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon, entry_id): + def __init__(self, openuv, sensor_type, name, icon): """Initialize the sensor.""" - super().__init__(openuv) + super().__init__(openuv, sensor_type) - self._async_unsub_dispatcher_connect = None - self._entry_id = entry_id - self._icon = icon - self._latitude = openuv.client.latitude - self._longitude = openuv.client.longitude - self._name = name - self._sensor_type = sensor_type - self._state = None - - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._latitude}_{self._longitude}_{self._sensor_type}" + self._attr_icon = icon + self._attr_name = name @callback def update_from_latest_data(self): @@ -76,10 +48,10 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): data = self.openuv.data[DATA_PROTECTION_WINDOW] if not data: - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True for key in ("from_time", "to_time", "from_uv", "to_uv"): if not data.get(key): @@ -87,12 +59,12 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): return if self._sensor_type == TYPE_PROTECTION_WINDOW: - self._state = ( + self._attr_is_on = ( parse_datetime(data["from_time"]) <= utcnow() <= parse_datetime(data["to_time"]) ) - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_PROTECTION_WINDOW_ENDING_TIME: as_local( parse_datetime(data["to_time"]) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 654a89cfcf9..6f4e4e18d34 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors = [] for kind, attrs in SENSORS.items(): name, icon, unit = attrs - sensors.append(OpenUvSensor(openuv, kind, name, icon, unit, entry.entry_id)) + sensors.append(OpenUvSensor(openuv, kind, name, icon, unit)) async_add_entities(sensors, True) @@ -91,44 +91,13 @@ async def async_setup_entry(hass, entry, async_add_entities): class OpenUvSensor(OpenUvEntity, SensorEntity): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon, unit, entry_id): + def __init__(self, openuv, sensor_type, name, icon, unit): """Initialize the sensor.""" - super().__init__(openuv) + super().__init__(openuv, sensor_type) - self._async_unsub_dispatcher_connect = None - self._entry_id = entry_id - self._icon = icon - self._latitude = openuv.client.latitude - self._longitude = openuv.client.longitude - self._name = name - self._sensor_type = sensor_type - self._state = None - self._unit = unit - - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def state(self): - """Return the status of the sensor.""" - return self._state - - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._latitude}_{self._longitude}_{self._sensor_type}" - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit + self._attr_icon = icon + self._attr_name = name + self._attr_unit_of_measurement = unit @callback def update_from_latest_data(self): @@ -136,29 +105,29 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): data = self.openuv.data[DATA_UV].get("result") if not data: - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL: - self._state = data["ozone"] + self._attr_state = data["ozone"] elif self._sensor_type == TYPE_CURRENT_UV_INDEX: - self._state = data["uv"] + self._attr_state = data["uv"] elif self._sensor_type == TYPE_CURRENT_UV_LEVEL: if data["uv"] >= 11: - self._state = UV_LEVEL_EXTREME + self._attr_state = UV_LEVEL_EXTREME elif data["uv"] >= 8: - self._state = UV_LEVEL_VHIGH + self._attr_state = UV_LEVEL_VHIGH elif data["uv"] >= 6: - self._state = UV_LEVEL_HIGH + self._attr_state = UV_LEVEL_HIGH elif data["uv"] >= 3: - self._state = UV_LEVEL_MODERATE + self._attr_state = UV_LEVEL_MODERATE else: - self._state = UV_LEVEL_LOW + self._attr_state = UV_LEVEL_LOW elif self._sensor_type == TYPE_MAX_UV_INDEX: - self._state = data["uv_max"] - self._attrs.update( + self._attr_state = data["uv_max"] + self._attr_extra_state_attributes.update( {ATTR_MAX_UV_TIME: as_local(parse_datetime(data["uv_max_time"]))} ) elif self._sensor_type in ( @@ -169,6 +138,6 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, ): - self._state = data["safe_exposure_time"][ + self._attr_state = data["safe_exposure_time"][ EXPOSURE_TYPE_MAP[self._sensor_type] ] From 16d2dcbfb22c4e6af2e45a4654ede929eef33202 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 09:51:47 +0200 Subject: [PATCH 031/818] Improve sensor statistics tests (#52386) --- .../components/recorder/statistics.py | 20 +- homeassistant/components/sensor/recorder.py | 8 + tests/components/sensor/test_recorder.py | 532 +++++++++++++----- 3 files changed, 403 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 753a66926ad..0e01005c13a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -45,6 +45,8 @@ QUERY_STATISTIC_META = [ STATISTICS_BAKERY = "recorder_statistics_bakery" STATISTICS_META_BAKERY = "recorder_statistics_bakery" +# Convert pressure and temperature statistics from the native unit used for statistics +# to the units configured by the user UNIT_CONVERSIONS = { PRESSURE_PA: lambda x, units: pressure_util.convert( x, PRESSURE_PA, units.pressure_unit @@ -137,7 +139,8 @@ def _get_meta_data(hass, session, statistic_ids): return {id: _meta(result, id) for id in statistic_ids} -def _unit_system_unit(unit: str, units) -> str: +def _configured_unit(unit: str, units) -> str: + """Return the pressure and temperature units configured by the user.""" if unit == PRESSURE_PA: return units.pressure_unit if unit == TEMP_CELSIUS: @@ -146,7 +149,7 @@ def _unit_system_unit(unit: str, units) -> str: def list_statistic_ids(hass, statistic_type=None): - """Return statistic_ids.""" + """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: baked_query = hass.data[STATISTICS_BAKERY]( @@ -161,13 +164,14 @@ def list_statistic_ids(hass, statistic_type=None): baked_query += lambda q: q.order_by(Statistics.statistic_id) result = execute(baked_query(session)) - statistic_ids_list = [statistic_id[0] for statistic_id in result] - statistic_ids = _get_meta_data(hass, session, statistic_ids_list) - for statistic_id in statistic_ids.values(): - unit = _unit_system_unit(statistic_id["unit_of_measurement"], units) - statistic_id["unit_of_measurement"] = unit - return list(statistic_ids.values()) + statistic_ids = [statistic_id[0] for statistic_id in result] + meta_data = _get_meta_data(hass, session, statistic_ids) + for item in meta_data.values(): + unit = _configured_unit(item["unit_of_measurement"], units) + item["unit_of_measurement"] = unit + + return list(meta_data.values()) def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c7f3e6e3ecd..d9200bdf797 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -54,6 +54,7 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, } +# Normalized units which will be stored in the statistics table DEVICE_CLASS_UNITS = { DEVICE_CLASS_ENERGY: ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_POWER: POWER_WATT, @@ -62,14 +63,18 @@ DEVICE_CLASS_UNITS = { } UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { + # Convert energy to kWh DEVICE_CLASS_ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x, ENERGY_WATT_HOUR: lambda x: x / 1000, }, + # Convert power W DEVICE_CLASS_POWER: { POWER_WATT: lambda x: x, POWER_KILO_WATT: lambda x: x * 1000, }, + # Convert pressure to Pa + # Note: pressure_util.convert is bypassed to avoid redundant error checking DEVICE_CLASS_PRESSURE: { PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR], PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA], @@ -78,6 +83,8 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA], PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI], }, + # Convert temperature to °C + # Note: temperature_util.convert is bypassed to avoid redundant error checking DEVICE_CLASS_TEMPERATURE: { TEMP_CELSIUS: lambda x: x, TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, @@ -85,6 +92,7 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { }, } +# Keep track of entities for which a warning about unsupported unit has been logged WARN_UNSUPPORTED_UNIT = set() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 65346f1feba..99ede396381 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,32 +1,141 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta -from unittest.mock import patch, sentinel +from unittest.mock import patch +import pytest from pytest import approx from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat -from homeassistant.components.recorder.statistics import statistics_during_period -from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS +from homeassistant.components.recorder.statistics import ( + list_statistic_ids, + statistics_during_period, +) +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.components.recorder.common import wait_recording_done +ENERGY_SENSOR_ATTRIBUTES = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", +} +POWER_SENSOR_ATTRIBUTES = { + "device_class": "power", + "state_class": "measurement", + "unit_of_measurement": "kW", +} +PRESSURE_SENSOR_ATTRIBUTES = { + "device_class": "pressure", + "state_class": "measurement", + "unit_of_measurement": "hPa", +} +TEMPERATURE_SENSOR_ATTRIBUTES = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°C", +} -def test_compile_hourly_statistics(hass_recorder): + +@pytest.mark.parametrize( + "device_class,unit,native_unit,mean,min,max", + [ + ("battery", "%", "%", 16.440677, 10, 30), + ("battery", None, None, 16.440677, 10, 30), + ("humidity", "%", "%", 16.440677, 10, 30), + ("humidity", None, None, 16.440677, 10, 30), + ("pressure", "Pa", "Pa", 16.440677, 10, 30), + ("pressure", "hPa", "Pa", 1644.0677, 1000, 3000), + ("pressure", "mbar", "Pa", 1644.0677, 1000, 3000), + ("pressure", "inHg", "Pa", 55674.53, 33863.89, 101591.67), + ("pressure", "psi", "Pa", 113354.48, 68947.57, 206842.71), + ("temperature", "°C", "°C", 16.440677, 10, 30), + ("temperature", "°F", "°C", -8.644068, -12.22222, -1.111111), + ], +) +def test_compile_hourly_statistics( + hass_recorder, caplog, device_class, unit, native_unit, mean, min, max +): """Test compiling hourly statistics.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states(hass) + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) recorder.do_adhoc_statistics(period="hourly", start=zero) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +@pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES]) +def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes): + """Test compiling hourly statistics for unsupported sensor.""" + attributes = dict(attributes) + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + four, states = record_states(hass, zero, "sensor.test1", attributes) + if "unit_of_measurement" in attributes: + attributes["unit_of_measurement"] = "invalid" + _, _states = record_states(hass, zero, "sensor.test2", attributes) + states = {**states, **_states} + attributes.pop("unit_of_measurement") + _, _states = record_states(hass, zero, "sensor.test3", attributes) + states = {**states, **_states} + attributes["state_class"] = "invalid" + _, _states = record_states(hass, zero, "sensor.test4", attributes) + states = {**states, **_states} + attributes.pop("state_class") + _, _states = record_states(hass, zero, "sensor.test5", attributes) + states = {**states, **_states} + attributes["state_class"] = "measurement" + _, _states = record_states(hass, zero, "sensor.test6", attributes) + states = {**states, **_states} + attributes["state_class"] = "unsupported" + _, _states = record_states(hass, zero, "sensor.test7", attributes) + states = {**states, **_states} + + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": "°C"} + ] stats = statistics_during_period(hass, zero) assert stats == { "sensor.test1": [ @@ -42,23 +151,36 @@ def test_compile_hourly_statistics(hass_recorder): } ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_energy_statistics(hass_recorder): +@pytest.mark.parametrize( + "device_class,unit,native_unit,factor", + [ + ("energy", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", 1 / 1000), + ("monetary", "€", "€", 1), + ("monetary", "SEK", "SEK", 1), + ], +) +def test_compile_hourly_energy_statistics( + hass_recorder, caplog, device_class, unit, native_unit, factor +): """Test compiling hourly statistics.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - sns1_attr = { - "device_class": "energy", + attributes = { + "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": "kWh", + "unit_of_measurement": unit, + "last_reset": None, } - sns2_attr = {"device_class": "energy"} - sns3_attr = {} + seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] - zero, four, eight, states = record_energy_states( - hass, sns1_attr, sns2_attr, sns3_attr + four, eight, states = record_energy_states( + hass, zero, "sensor.test1", attributes, seq ) hist = history.get_significant_states( hass, zero - timedelta.resolution, eight + timedelta.resolution @@ -71,6 +193,97 @@ def test_compile_hourly_energy_statistics(hass_recorder): wait_recording_done(hass) recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": approx(factor * seq[2]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(factor * seq[5]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(factor * seq[8]), + "sum": approx(factor * 40.0), + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): + """Test compiling hourly statistics.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + sns1_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + "last_reset": None, + } + sns2_attr = {"device_class": "energy"} + sns3_attr = {} + sns4_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + } + seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70] + seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75] + seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] + seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] + + four, eight, states = record_energy_states( + hass, zero, "sensor.test1", sns1_attr, seq1 + ) + _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + states = {**states, **_states} + + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"} + ] stats = statistics_during_period(hass, zero) assert stats == { "sensor.test1": [ @@ -106,32 +319,37 @@ def test_compile_hourly_energy_statistics(hass_recorder): }, ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_energy_statistics2(hass_recorder): - """Test compiling hourly statistics.""" +def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): + """Test compiling multiple hourly statistics.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - sns1_attr = { - "device_class": "energy", - "state_class": "measurement", - "unit_of_measurement": "kWh", - } - sns2_attr = { - "device_class": "energy", - "state_class": "measurement", - "unit_of_measurement": "kWh", - } + sns1_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} + sns2_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} sns3_attr = { - "device_class": "energy", - "state_class": "measurement", + **ENERGY_SENSOR_ATTRIBUTES, "unit_of_measurement": "Wh", + "last_reset": None, } + sns4_attr = {**ENERGY_SENSOR_ATTRIBUTES} + seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70] + seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75] + seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] + seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] - zero, four, eight, states = record_energy_states( - hass, sns1_attr, sns2_attr, sns3_attr + four, eight, states = record_energy_states( + hass, zero, "sensor.test1", sns1_attr, seq1 ) + _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + states = {**states, **_states} hist = history.get_significant_states( hass, zero - timedelta.resolution, eight + timedelta.resolution ) @@ -143,6 +361,12 @@ def test_compile_hourly_energy_statistics2(hass_recorder): wait_recording_done(hass) recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"}, + {"statistic_id": "sensor.test2", "unit_of_measurement": "kWh"}, + {"statistic_id": "sensor.test3", "unit_of_measurement": "kWh"}, + ] stats = statistics_during_period(hass, zero) assert stats == { "sensor.test1": [ @@ -242,14 +466,39 @@ def test_compile_hourly_energy_statistics2(hass_recorder): }, ], } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_unchanged(hass_recorder): +@pytest.mark.parametrize( + "device_class,unit,value", + [ + ("battery", "%", 30), + ("battery", None, 30), + ("humidity", "%", 30), + ("humidity", None, 30), + ("pressure", "Pa", 30), + ("pressure", "hPa", 3000), + ("pressure", "mbar", 3000), + ("pressure", "inHg", 101591.67), + ("pressure", "psi", 206842.71), + ("temperature", "°C", 30), + ("temperature", "°F", -1.111111), + ], +) +def test_compile_hourly_statistics_unchanged( + hass_recorder, caplog, device_class, unit, value +): """Test compiling hourly statistics, with no changes during the hour.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states(hass) + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) @@ -261,23 +510,27 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": approx(30.0), - "min": approx(30.0), - "max": approx(30.0), + "mean": approx(value), + "min": approx(value), + "max": approx(value), "last_reset": None, "state": None, "sum": None, } ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_partially_unavailable(hass_recorder): +def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): """Test compiling hourly statistics, with the sensor being partially unavailable.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states_partially_unavailable(hass) + four, states = record_states_partially_unavailable( + hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES + ) hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) @@ -298,39 +551,87 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): } ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_unavailable(hass_recorder): +@pytest.mark.parametrize( + "device_class,unit,value", + [ + ("battery", "%", 30), + ("battery", None, 30), + ("humidity", "%", 30), + ("humidity", None, 30), + ("pressure", "Pa", 30), + ("pressure", "hPa", 3000), + ("pressure", "mbar", 3000), + ("pressure", "inHg", 101591.67), + ("pressure", "psi", 206842.71), + ("temperature", "°C", 30), + ("temperature", "°F", -1.111111), + ], +) +def test_compile_hourly_statistics_unavailable( + hass_recorder, caplog, device_class, unit, value +): """Test compiling hourly statistics, with the sensor being unavailable.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states_partially_unavailable(hass) + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": unit, + } + four, states = record_states_partially_unavailable( + hass, zero, "sensor.test1", attributes + ) + _, _states = record_states(hass, zero, "sensor.test2", attributes) + states = {**states, **_states} hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) recorder.do_adhoc_statistics(period="hourly", start=four) wait_recording_done(hass) stats = statistics_during_period(hass, four) - assert stats == {} + assert stats == { + "sensor.test2": [ + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(four), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text -def record_states(hass): +def test_compile_hourly_statistics_fails(hass_recorder, caplog): + """Test compiling hourly statistics throws.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + with patch( + "homeassistant.components.sensor.recorder.compile_statistics", + side_effect=Exception, + ): + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + assert "Error while processing event StatisticsTask" in caplog.text + + +def record_states(hass, zero, entity_id, attributes): """Record some test states. We inject a bunch of state updates for temperature sensors. """ - mp = "media_player.test" - sns1 = "sensor.test1" - sns2 = "sensor.test2" - sns3 = "sensor.test3" - sns1_attr = { - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": TEMP_CELSIUS, - } - sns2_attr = {"device_class": "temperature"} - sns3_attr = {} + attributes = dict(attributes) def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -338,46 +639,29 @@ def record_states(hass): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() one = zero + timedelta(minutes=1) two = one + timedelta(minutes=10) three = two + timedelta(minutes=40) four = three + timedelta(minutes=10) - states = {mp: [], sns1: [], sns2: [], sns3: []} + states = {entity_id: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): - states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "10", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): - states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "15", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "30", attributes=attributes)) - return zero, four, states + return four, states -def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): +def record_energy_states(hass, zero, entity_id, _attributes, seq): """Record some test states. We inject a bunch of state updates for energy sensors. """ - sns1 = "sensor.test1" - sns2 = "sensor.test2" - sns3 = "sensor.test3" - sns4 = "sensor.test4" def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -385,7 +669,6 @@ def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() one = zero + timedelta(minutes=15) two = one + timedelta(minutes=30) three = two + timedelta(minutes=15) @@ -395,88 +678,50 @@ def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): seven = six + timedelta(minutes=15) eight = seven + timedelta(minutes=30) - sns1_attr = {**_sns1_attr, "last_reset": zero.isoformat()} - sns2_attr = {**_sns2_attr, "last_reset": zero.isoformat()} - sns3_attr = {**_sns3_attr, "last_reset": zero.isoformat()} - sns4_attr = {**_sns3_attr} + attributes = dict(_attributes) + if "last_reset" in _attributes: + attributes["last_reset"] = zero.isoformat() - states = {sns1: [], sns2: [], sns3: [], sns4: []} + states = {entity_id: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero): - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 - states[sns2].append(set_state(sns2, "110", attributes=sns2_attr)) # Sum 0 - states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 - states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): - states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) # Sum 5 - states[sns2].append(set_state(sns2, "120", attributes=sns2_attr)) # Sum 10 - states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 - states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[1], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): - states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) # Sum 10 - states[sns2].append(set_state(sns2, "130", attributes=sns2_attr)) # Sum 20 - states[sns3].append(set_state(sns3, "5", attributes=sns3_attr)) # Sum 5 - states[sns4].append(set_state(sns4, "5", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[2], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 - states[sns2].append(set_state(sns2, "0", attributes=sns2_attr)) # Sum -110 - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) # Sum 10 - states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[3], attributes=attributes)) - sns1_attr = {**_sns1_attr, "last_reset": four.isoformat()} - sns2_attr = {**_sns2_attr, "last_reset": four.isoformat()} - sns3_attr = {**_sns3_attr, "last_reset": four.isoformat()} + attributes = dict(_attributes) + if "last_reset" in _attributes: + attributes["last_reset"] = four.isoformat() with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four): - states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) # Sum 0 - states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) # Sum -110 - states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) # Sum 10 - states[sns4].append(set_state(sns4, "30", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[4], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=five): - states[sns1].append(set_state(sns1, "40", attributes=sns1_attr)) # Sum 10 - states[sns2].append(set_state(sns2, "45", attributes=sns2_attr)) # Sum -95 - states[sns3].append(set_state(sns3, "50", attributes=sns3_attr)) # Sum 30 - states[sns4].append(set_state(sns4, "50", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[5], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=six): - states[sns1].append(set_state(sns1, "50", attributes=sns1_attr)) # Sum 20 - states[sns2].append(set_state(sns2, "55", attributes=sns2_attr)) # Sum -85 - states[sns3].append(set_state(sns3, "60", attributes=sns3_attr)) # Sum 40 - states[sns4].append(set_state(sns4, "60", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[6], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=seven): - states[sns1].append(set_state(sns1, "60", attributes=sns1_attr)) # Sum 30 - states[sns2].append(set_state(sns2, "65", attributes=sns2_attr)) # Sum -75 - states[sns3].append(set_state(sns3, "80", attributes=sns3_attr)) # Sum 60 - states[sns4].append(set_state(sns4, "80", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[7], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=eight): - states[sns1].append(set_state(sns1, "70", attributes=sns1_attr)) # Sum 40 - states[sns2].append(set_state(sns2, "75", attributes=sns2_attr)) # Sum -65 - states[sns3].append(set_state(sns3, "90", attributes=sns3_attr)) # Sum 70 + states[entity_id].append(set_state(entity_id, seq[8], attributes=attributes)) - return zero, four, eight, states + return four, eight, states -def record_states_partially_unavailable(hass): +def record_states_partially_unavailable(hass, zero, entity_id, attributes): """Record some test states. We inject a bunch of state updates temperature sensors. """ - mp = "media_player.test" - sns1 = "sensor.test1" - sns2 = "sensor.test2" - sns3 = "sensor.test3" - sns1_attr = { - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": TEMP_CELSIUS, - } - sns2_attr = {"device_class": "temperature"} - sns3_attr = {} def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -484,32 +729,21 @@ def record_states_partially_unavailable(hass): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() one = zero + timedelta(minutes=1) two = one + timedelta(minutes=15) three = two + timedelta(minutes=30) four = three + timedelta(minutes=15) - states = {mp: [], sns1: [], sns2: [], sns3: []} + states = {entity_id: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): - states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "10", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): - states[sns1].append(set_state(sns1, "25", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "25", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "25", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "25", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, STATE_UNAVAILABLE, attributes=sns1_attr)) - states[sns2].append(set_state(sns2, STATE_UNAVAILABLE, attributes=sns2_attr)) - states[sns3].append(set_state(sns3, STATE_UNAVAILABLE, attributes=sns3_attr)) + states[entity_id].append( + set_state(entity_id, STATE_UNAVAILABLE, attributes=attributes) + ) - return zero, four, states + return four, states From 14d3286b2113614f33bb3c34aa90764acf62b261 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 2 Jul 2021 10:08:54 +0200 Subject: [PATCH 032/818] Clean up netatmo sensor data processing (#52403) --- homeassistant/components/netatmo/sensor.py | 175 +++++++++++++-------- 1 file changed, 107 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 2dbbeb56c76..c059bb6bb4b 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -50,66 +50,136 @@ SUPPORTED_PUBLIC_SENSOR_TYPES = [ "sum_rain_24", ] +# sensor type: [name, netatmo name, unit of measurement, icon, device class, enable default] SENSOR_TYPES = { - "temperature": ["Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, True], - "temp_trend": ["Temperature trend", None, "mdi:trending-up", None, False], - "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, None, DEVICE_CLASS_CO2, True], - "pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE, True], - "pressure_trend": ["Pressure trend", None, "mdi:trending-up", None, False], - "noise": ["Noise", "dB", "mdi:volume-high", None, True], - "humidity": ["Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, True], - "rain": ["Rain", LENGTH_MILLIMETERS, "mdi:weather-rainy", None, True], + "temperature": [ + "Temperature", + "Temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + True, + ], + "temp_trend": [ + "Temperature trend", + "temp_trend", + None, + "mdi:trending-up", + None, + False, + ], + "co2": [ + "CO2", + "CO2", + CONCENTRATION_PARTS_PER_MILLION, + None, + DEVICE_CLASS_CO2, + True, + ], + "pressure": [ + "Pressure", + "Pressure", + PRESSURE_MBAR, + None, + DEVICE_CLASS_PRESSURE, + True, + ], + "pressure_trend": [ + "Pressure trend", + "pressure_trend", + None, + "mdi:trending-up", + None, + False, + ], + "noise": ["Noise", "Noise", "dB", "mdi:volume-high", None, True], + "humidity": ["Humidity", "Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, True], + "rain": ["Rain", "Rain", LENGTH_MILLIMETERS, "mdi:weather-rainy", None, True], "sum_rain_1": [ "Rain last hour", + "sum_rain_1", LENGTH_MILLIMETERS, "mdi:weather-rainy", None, False, ], - "sum_rain_24": ["Rain today", LENGTH_MILLIMETERS, "mdi:weather-rainy", None, True], + "sum_rain_24": [ + "Rain today", + "sum_rain_24", + LENGTH_MILLIMETERS, + "mdi:weather-rainy", + None, + True, + ], "battery_percent": [ "Battery Percent", + "battery_percent", PERCENTAGE, None, DEVICE_CLASS_BATTERY, True, ], - "windangle": ["Direction", None, "mdi:compass-outline", None, True], - "windangle_value": ["Angle", DEGREE, "mdi:compass-outline", None, False], + "windangle": ["Direction", "WindAngle", None, "mdi:compass-outline", None, True], + "windangle_value": [ + "Angle", + "WindAngle", + DEGREE, + "mdi:compass-outline", + None, + False, + ], "windstrength": [ "Wind Strength", + "WindStrength", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy", None, True, ], - "gustangle": ["Gust Direction", None, "mdi:compass-outline", None, False], - "gustangle_value": ["Gust Angle", DEGREE, "mdi:compass-outline", None, False], + "gustangle": [ + "Gust Direction", + "GustAngle", + None, + "mdi:compass-outline", + None, + False, + ], + "gustangle_value": [ + "Gust Angle", + "GustAngle", + DEGREE, + "mdi:compass-outline", + None, + False, + ], "guststrength": [ "Gust Strength", + "GustStrength", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy", None, False, ], - "reachable": ["Reachability", None, "mdi:signal", None, False], - "rf_status": ["Radio", None, "mdi:signal", None, False], + "reachable": ["Reachability", "reachable", None, "mdi:signal", None, False], + "rf_status": ["Radio", "rf_status", None, "mdi:signal", None, False], "rf_status_lvl": [ "Radio Level", + "rf_status", SIGNAL_STRENGTH_DECIBELS_MILLIWATT, None, DEVICE_CLASS_SIGNAL_STRENGTH, False, ], - "wifi_status": ["Wifi", None, "mdi:wifi", None, False], + "wifi_status": ["Wifi", "wifi_status", None, "mdi:wifi", None, False], "wifi_status_lvl": [ "Wifi Level", + "wifi_status", SIGNAL_STRENGTH_DECIBELS_MILLIWATT, None, DEVICE_CLASS_SIGNAL_STRENGTH, False, ], - "health_idx": ["Health", None, "mdi:cloud", None, True], + "health_idx": ["Health", "health_idx", None, "mdi:cloud", None, True], } MODULE_TYPE_OUTDOOR = "NAModule1" @@ -287,12 +357,12 @@ class NetatmoSensor(NetatmoBase, SensorEntity): ) self.type = sensor_type self._state = None - self._device_class = SENSOR_TYPES[self.type][3] - self._icon = SENSOR_TYPES[self.type][2] - self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._device_class = SENSOR_TYPES[self.type][4] + self._icon = SENSOR_TYPES[self.type][3] + self._unit_of_measurement = SENSOR_TYPES[self.type][2] self._model = device["type"] self._unique_id = f"{self._id}-{self.type}" - self._enabled_default = SENSOR_TYPES[self.type][4] + self._enabled_default = SENSOR_TYPES[self.type][5] @property def icon(self): @@ -325,7 +395,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): return self._enabled_default @callback - def async_update_callback(self): # noqa: C901 + def async_update_callback(self): """Update the entity's state.""" if self._data is None: if self._state is None: @@ -350,52 +420,21 @@ class NetatmoSensor(NetatmoBase, SensorEntity): return try: - if self.type == "temperature": - self._state = round(data["Temperature"], 1) - elif self.type == "temp_trend": - self._state = data["temp_trend"] - elif self.type == "humidity": - self._state = data["Humidity"] - elif self.type == "rain": - self._state = data["Rain"] - elif self.type == "sum_rain_1": - self._state = round(data["sum_rain_1"], 1) - elif self.type == "sum_rain_24": - self._state = data["sum_rain_24"] - elif self.type == "noise": - self._state = data["Noise"] - elif self.type == "co2": - self._state = data["CO2"] - elif self.type == "pressure": - self._state = round(data["Pressure"], 1) - elif self.type == "pressure_trend": - self._state = data["pressure_trend"] - elif self.type == "battery_percent": - self._state = data["battery_percent"] - elif self.type == "windangle_value": - self._state = fix_angle(data["WindAngle"]) - elif self.type == "windangle": - self._state = process_angle(fix_angle(data["WindAngle"])) - elif self.type == "windstrength": - self._state = data["WindStrength"] - elif self.type == "gustangle_value": - self._state = fix_angle(data["GustAngle"]) - elif self.type == "gustangle": - self._state = process_angle(fix_angle(data["GustAngle"])) - elif self.type == "guststrength": - self._state = data["GustStrength"] - elif self.type == "reachable": - self._state = data["reachable"] - elif self.type == "rf_status_lvl": - self._state = data["rf_status"] + state = data[SENSOR_TYPES[self.type][1]] + if self.type in {"temperature", "pressure", "sum_rain_1"}: + self._state = round(state, 1) + elif self.type in {"windangle_value", "gustangle_value"}: + self._state = fix_angle(state) + elif self.type in {"windangle", "gustangle"}: + self._state = process_angle(fix_angle(state)) elif self.type == "rf_status": - self._state = process_rf(data["rf_status"]) - elif self.type == "wifi_status_lvl": - self._state = data["wifi_status"] + self._state = process_rf(state) elif self.type == "wifi_status": - self._state = process_wifi(data["wifi_status"]) + self._state = process_wifi(state) elif self.type == "health_idx": - self._state = process_health(data["health_idx"]) + self._state = process_health(state) + else: + self._state = state except KeyError: if self._state: _LOGGER.debug("No %s data found for %s", self.type, self._device_name) @@ -513,9 +552,9 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._device_name = f"{self._area_name}" self._name = f"{MANUFACTURER} {self._device_name} {SENSOR_TYPES[self.type][0]}" self._state = None - self._device_class = SENSOR_TYPES[self.type][3] - self._icon = SENSOR_TYPES[self.type][2] - self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._device_class = SENSOR_TYPES[self.type][4] + self._icon = SENSOR_TYPES[self.type][3] + self._unit_of_measurement = SENSOR_TYPES[self.type][2] self._show_on_map = area.show_on_map self._unique_id = f"{self._device_name.replace(' ', '-')}-{self.type}" self._model = PUBLIC From b3a625593e2eba65e51283fe3f5fca4b5e619769 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Fri, 2 Jul 2021 10:15:05 +0100 Subject: [PATCH 033/818] Add update listener to Coinbase (#52404) Co-authored-by: Franck Nijhof --- homeassistant/components/coinbase/__init__.py | 26 +++++++++++++++++ homeassistant/components/coinbase/sensor.py | 16 +++++++---- tests/components/coinbase/const.py | 4 +-- tests/components/coinbase/test_config_flow.py | 28 ++++++++++++++++--- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index eb4370a9534..08b97756dff 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -70,6 +71,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_and_update_instance, entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN] ) + entry.async_on_unload(entry.add_update_listener(update_listener)) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = instance @@ -96,6 +99,29 @@ def create_and_update_instance(api_key, api_token): return instance +async def update_listener(hass, config_entry): + """Handle options update.""" + + await hass.config_entries.async_reload(config_entry.entry_id) + + registry = entity_registry.async_get(hass) + entities = entity_registry.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + + # Remove orphaned entities + for entity in entities: + currency = entity.unique_id.split("-")[-1] + if "xe" in entity.unique_id and currency not in config_entry.options.get( + CONF_EXCHANGE_RATES + ): + registry.async_remove(entity.entity_id) + elif "wallet" in entity.unique_id and currency not in config_entry.options.get( + CONF_CURRENCIES + ): + registry.async_remove(entity.entity_id) + + def get_accounts(client): """Handle paginated accounts.""" response = client.get_accounts() diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 5febfe8a978..13981619051 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -11,6 +11,7 @@ from .const import ( API_ACCOUNT_ID, API_ACCOUNT_NAME, API_ACCOUNT_NATIVE_BALANCE, + API_RATES, CONF_CURRENCIES, CONF_EXCHANGE_RATES, DOMAIN, @@ -48,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if CONF_CURRENCIES in config_entry.options: desired_currencies = config_entry.options[CONF_CURRENCIES] - exchange_native_currency = instance.exchange_rates.currency + exchange_native_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY] for currency in desired_currencies: if currency not in provided_currencies: @@ -81,9 +82,12 @@ class AccountSensor(SensorEntity): self._coinbase_data = coinbase_data self._currency = currency for account in coinbase_data.accounts: - if account.currency == currency: + if account[API_ACCOUNT_CURRENCY] == currency: self._name = f"Coinbase {account[API_ACCOUNT_NAME]}" - self._id = f"coinbase-{account[API_ACCOUNT_ID]}" + self._id = ( + f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" + f"{account[API_ACCOUNT_CURRENCY]}" + ) self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] self._unit_of_measurement = account[API_ACCOUNT_CURRENCY] self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ @@ -131,7 +135,7 @@ class AccountSensor(SensorEntity): """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: - if account.currency == self._currency: + if account[API_ACCOUNT_CURRENCY] == self._currency: self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ API_ACCOUNT_AMOUNT @@ -150,9 +154,9 @@ class ExchangeRateSensor(SensorEntity): self._coinbase_data = coinbase_data self.currency = exchange_currency self._name = f"{exchange_currency} Exchange Rate" - self._id = f"{coinbase_data.user_id}-xe-{exchange_currency}" + self._id = f"coinbase-{coinbase_data.user_id}-xe-{exchange_currency}" self._state = round( - 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2 + 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) self._unit_of_measurement = native_currency diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 52505be514e..864ebc18701 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -11,14 +11,14 @@ BAD_EXCHANGE_RATE = "ETH" MOCK_ACCOUNTS_RESPONSE = [ { "balance": {"amount": "13.38", "currency": GOOD_CURRENCY_3}, - "currency": "BTC", + "currency": GOOD_CURRENCY_3, "id": "ABCDEF", "name": "BTC Wallet", "native_balance": {"amount": "15.02", "currency": GOOD_CURRENCY_2}, }, { "balance": {"amount": "0.00001", "currency": GOOD_CURRENCY}, - "currency": "BTC", + "currency": GOOD_CURRENCY, "id": "123456789", "name": "BTC Wallet", "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index dc036d23a6f..4c7b6c13333 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -19,7 +19,14 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) -from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE +from .const import ( + BAD_CURRENCY, + BAD_EXCHANGE_RATE, + GOOD_CURRENCY, + GOOD_CURRENCY_2, + GOOD_EXCHNAGE_RATE, + GOOD_EXCHNAGE_RATE_2, +) from tests.common import MockConfigEntry @@ -139,6 +146,18 @@ async def test_form_catch_all_exception(hass): async def test_option_good_account_currency(hass): """Test we handle a good wallet currency option.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + title="Test User", + data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, + options={ + CONF_CURRENCIES: [GOOD_CURRENCY_2], + CONF_EXCHANGE_RATES: [], + }, + ) + config_entry.add_to_hass(hass) + with patch( "coinbase.wallet.client.Client.get_current_user", return_value=mock_get_current_user(), @@ -148,7 +167,8 @@ async def test_option_good_account_currency(hass): "coinbase.wallet.client.Client.get_exchange_rates", return_value=mock_get_exchange_rates(), ): - config_entry = await init_mock_coinbase(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() result2 = await hass.config_entries.options.async_configure( @@ -191,12 +211,12 @@ async def test_option_good_exchange_rate(hass): """Test we handle a good exchange rate option.""" config_entry = MockConfigEntry( domain=DOMAIN, - unique_id="abcde12345", + entry_id="abcde12345", title="Test User", data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, options={ CONF_CURRENCIES: [], - CONF_EXCHANGE_RATES: [], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE_2], }, ) config_entry.add_to_hass(hass) From 2ed1efc3a3b66bc3bd51fdff9d00f1ebbf598524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Krollmann?= Date: Fri, 2 Jul 2021 11:20:00 +0200 Subject: [PATCH 034/818] Add rainbird set_rain_delay service (#52369) Co-authored-by: Franck Nijhof --- .../components/rainbird/services.yaml | 13 +++++++++++++ homeassistant/components/rainbird/switch.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index e1fa1879549..3d5f55dba14 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -19,3 +19,16 @@ start_irrigation: min: 1 max: 1440 unit_of_measurement: "minutes" +set_rain_delay: + name: Set rain delay + description: Set how long automatic irrigation is turned off. + fields: + duration: + name: Duration + description: Duration for this system to be turned off. + required: true + selector: + number: + min: 0 + max: 14 + unit_of_measurement: "days" diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 7acb9740616..df83f054275 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -11,6 +11,7 @@ from . import CONF_ZONES, DATA_RAINBIRD, DOMAIN, RAINBIRD_CONTROLLER ATTR_DURATION = "duration" SERVICE_START_IRRIGATION = "start_irrigation" +SERVICE_SET_RAIN_DELAY = "set_rain_delay" SERVICE_SCHEMA_IRRIGATION = vol.Schema( { @@ -19,6 +20,12 @@ SERVICE_SCHEMA_IRRIGATION = vol.Schema( } ) +SERVICE_SCHEMA_RAIN_DELAY = vol.Schema( + { + vol.Required(ATTR_DURATION): cv.positive_float, + } +) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Rain Bird switches over a Rain Bird controller.""" @@ -64,6 +71,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): schema=SERVICE_SCHEMA_IRRIGATION, ) + def set_rain_delay(service): + duration = service.data[ATTR_DURATION] + + controller.set_rain_delay(duration) + + hass.services.register( + DOMAIN, + SERVICE_SET_RAIN_DELAY, + set_rain_delay, + schema=SERVICE_SCHEMA_RAIN_DELAY, + ) + class RainBirdSwitch(SwitchEntity): """Representation of a Rain Bird switch.""" From 7291228e1617428fe03dbc0f05268ab5a1a701ad Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 2 Jul 2021 11:36:37 +0200 Subject: [PATCH 035/818] Remove boilerplate code in favour of attributes in Netatmo integration (#52395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make use of attributes * Add suggestions * Update homeassistant/components/netatmo/sensor.py Co-authored-by: Daniel Hjelseth Høyer * Update homeassistant/components/netatmo/sensor.py Co-authored-by: Daniel Hjelseth Høyer Co-authored-by: Daniel Hjelseth Høyer --- homeassistant/components/netatmo/camera.py | 36 ++--- homeassistant/components/netatmo/climate.py | 38 +++--- homeassistant/components/netatmo/const.py | 1 + homeassistant/components/netatmo/light.py | 8 +- .../components/netatmo/netatmo_entity_base.py | 25 ++-- homeassistant/components/netatmo/sensor.py | 129 ++++++------------ 6 files changed, 92 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 7004ef0c472..431aa814bde 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -125,9 +125,9 @@ class NetatmoCamera(NetatmoBase, Camera): self._id = camera_id self._home_id = home_id self._device_name = self._data.get_camera(camera_id=camera_id).get("name") - self._name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model = camera_type - self._unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self._id}-{self._model}" self._quality = quality self._vpnurl = None self._localurl = None @@ -172,6 +172,9 @@ class NetatmoCamera(NetatmoBase, Camera): self._status = "on" elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] + self._attr_extra_state_attributes.update( + {"light_state": self._light_state} + ) self.async_write_ha_state() return @@ -190,20 +193,6 @@ class NetatmoCamera(NetatmoBase, Camera): _LOGGER.debug("Could not fetch live camera image (%s)", err) return None - @property - def extra_state_attributes(self): - """Return the Netatmo-specific camera state attributes.""" - return { - "id": self._id, - "status": self._status, - "sd_status": self._sd_status, - "alim_status": self._alim_status, - "is_local": self._is_local, - "vpn_url": self._vpnurl, - "local_url": self._localurl, - "light_state": self._light_state, - } - @property def available(self): """Return True if entity is available.""" @@ -273,6 +262,19 @@ class NetatmoCamera(NetatmoBase, Camera): self._data.outdoor_events.get(self._id, {}) ) + self._attr_extra_state_attributes.update( + { + "id": self._id, + "status": self._status, + "sd_status": self._sd_status, + "alim_status": self._alim_status, + "is_local": self._is_local, + "vpn_url": self._vpnurl, + "local_url": self._localurl, + "light_state": self._light_state, + } + ) + def process_events(self, events): """Add meta data to events.""" for event in events.values(): @@ -328,7 +330,7 @@ class NetatmoCamera(NetatmoBase, Camera): async def _service_set_camera_light(self, **kwargs): """Service to set light mode.""" mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) - _LOGGER.debug("Turn %s camera light for '%s'", mode, self._name) + _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index ce1eba11b70..c041370638c 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -198,7 +198,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): break self._device_name = self._data.rooms[home_id][room_id]["name"] - self._name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = f"{MANUFACTURER} {self._device_name}" self._current_temperature = None self._target_temperature = None self._preset = None @@ -218,7 +218,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if self._model == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) - self._unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self._id}-{self._model}" async def async_added_to_hass(self) -> None: """Entity created.""" @@ -253,6 +253,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._selected_schedule = self.hass.data[DOMAIN][DATA_SCHEDULES][ self._home_id ].get(data["schedule_id"]) + self._attr_extra_state_attributes.update( + {"selected_schedule": self._selected_schedule} + ) self.async_write_ha_state() self.data_handler.async_force_update(self._home_status_class) return @@ -426,24 +429,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() - @property - def extra_state_attributes(self): - """Return the state attributes of the thermostat.""" - attr = {} - - if self._battery_level is not None: - attr[ATTR_BATTERY_LEVEL] = self._battery_level - - if self._model == NA_VALVE: - attr[ATTR_HEATING_POWER_REQUEST] = self._room_status.get( - "heating_power_request", 0 - ) - - if self._selected_schedule is not None: - attr[ATTR_SELECTED_SCHEDULE] = self._selected_schedule - - return attr - async def async_turn_off(self): """Turn the entity off.""" if self._model == NA_VALVE: @@ -513,6 +498,19 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] + if self._battery_level is not None: + self._attr_extra_state_attributes[ATTR_BATTERY_LEVEL] = self._battery_level + + if self._model == NA_VALVE: + self._attr_extra_state_attributes[ + ATTR_HEATING_POWER_REQUEST + ] = self._room_status.get("heating_power_request", 0) + + if self._selected_schedule is not None: + self._attr_extra_state_attributes[ + ATTR_SELECTED_SCHEDULE + ] = self._selected_schedule + def _build_room_status(self): """Construct room status.""" try: diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 2f840baa4c3..e974d43134e 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -8,6 +8,7 @@ API = "api" DOMAIN = "netatmo" MANUFACTURER = "Netatmo" +DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN] diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 160fb00be6b..3ad9db8ae7c 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -80,9 +80,9 @@ class NetatmoLight(NetatmoBase, LightEntity): self._home_id = home_id self._model = camera_type self._device_name = self._data.get_camera(camera_id).get("name") - self._name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = f"{MANUFACTURER} {self._device_name}" self._is_on = False - self._unique_id = f"{self._id}-light" + self._attr_unique_id = f"{self._id}-light" async def async_added_to_hass(self) -> None: """Entity created.""" @@ -126,7 +126,7 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_on(self, **kwargs): """Turn camera floodlight on.""" - _LOGGER.debug("Turn camera '%s' on", self._name) + _LOGGER.debug("Turn camera '%s' on", self._attr_name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, @@ -135,7 +135,7 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_off(self, **kwargs): """Turn camera floodlight into auto mode.""" - _LOGGER.debug("Turn camera '%s' to auto mode", self._name) + _LOGGER.debug("Turn camera '%s' to auto mode", self._attr_name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 5d43a46e89b..b74975f3a62 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,10 +1,18 @@ """Base class for Netatmo entities.""" from __future__ import annotations +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity -from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME +from .const import ( + DATA_DEVICE_IDS, + DEFAULT_ATTRIBUTION, + DOMAIN, + MANUFACTURER, + MODELS, + SIGNAL_NAME, +) from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler @@ -20,8 +28,9 @@ class NetatmoBase(Entity): self._device_name = None self._id = None self._model = None - self._name = None - self._unique_id = None + self._attr_name = None + self._attr_unique_id = None + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} async def async_added_to_hass(self) -> None: """Entity created.""" @@ -84,16 +93,6 @@ class NetatmoBase(Entity): """Return data for this entity.""" return self.data_handler.data[self._data_classes[0]["name"]] - @property - def unique_id(self): - """Return the unique ID of this entity.""" - return self._unique_id - - @property - def name(self): - """Return the name of this entity.""" - return self._name - @property def device_info(self): """Return the device info for the sensor.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index c059bb6bb4b..e8e142d7f16 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -352,56 +352,30 @@ class NetatmoSensor(NetatmoBase, SensorEntity): f"{module_info.get('module_name', device['type'])}" ) - self._name = ( + self._attr_name = ( f"{MANUFACTURER} {self._device_name} {SENSOR_TYPES[sensor_type][0]}" ) self.type = sensor_type - self._state = None - self._device_class = SENSOR_TYPES[self.type][4] - self._icon = SENSOR_TYPES[self.type][3] - self._unit_of_measurement = SENSOR_TYPES[self.type][2] + self._attr_device_class = SENSOR_TYPES[self.type][4] + self._attr_icon = SENSOR_TYPES[self.type][3] + self._attr_unit_of_measurement = SENSOR_TYPES[self.type][2] self._model = device["type"] - self._unique_id = f"{self._id}-{self.type}" - self._enabled_default = SENSOR_TYPES[self.type][5] - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement + self._attr_unique_id = f"{self._id}-{self.type}" + self._attr_entity_registry_enabled_default = SENSOR_TYPES[self.type][5] @property def available(self): """Return entity availability.""" - return self._state is not None - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default + return self._attr_state is not None @callback def async_update_callback(self): """Update the entity's state.""" if self._data is None: - if self._state is None: + if self._attr_state is None: return _LOGGER.warning("No data from update") - self._state = None + self._attr_state = None return data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get( @@ -409,36 +383,36 @@ class NetatmoSensor(NetatmoBase, SensorEntity): ) if data is None: - if self._state: + if self._attr_state: _LOGGER.debug( "No data found for %s - %s (%s)", self.name, self._device_name, self._id, ) - self._state = None + self._attr_state = None return try: state = data[SENSOR_TYPES[self.type][1]] if self.type in {"temperature", "pressure", "sum_rain_1"}: - self._state = round(state, 1) + self._attr_state = round(state, 1) elif self.type in {"windangle_value", "gustangle_value"}: - self._state = fix_angle(state) + self._attr_state = fix_angle(state) elif self.type in {"windangle", "gustangle"}: - self._state = process_angle(fix_angle(state)) + self._attr_state = process_angle(fix_angle(state)) elif self.type == "rf_status": - self._state = process_rf(state) + self._attr_state = process_rf(state) elif self.type == "wifi_status": - self._state = process_wifi(state) + self._attr_state = process_wifi(state) elif self.type == "health_idx": - self._state = process_health(state) + self._attr_state = process_health(state) else: - self._state = state + self._attr_state = state except KeyError: - if self._state: + if self._attr_state: _LOGGER.debug("No %s data found for %s", self.type, self._device_name) - self._state = None + self._attr_state = None return self.async_write_ha_state() @@ -550,50 +524,22 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._name = f"{MANUFACTURER} {self._device_name} {SENSOR_TYPES[self.type][0]}" - self._state = None - self._device_class = SENSOR_TYPES[self.type][4] - self._icon = SENSOR_TYPES[self.type][3] - self._unit_of_measurement = SENSOR_TYPES[self.type][2] + self._attr_name = ( + f"{MANUFACTURER} {self._device_name} {SENSOR_TYPES[self.type][0]}" + ) + self._attr_device_class = SENSOR_TYPES[self.type][4] + self._attr_icon = SENSOR_TYPES[self.type][3] + self._attr_unit_of_measurement = SENSOR_TYPES[self.type][2] self._show_on_map = area.show_on_map - self._unique_id = f"{self._device_name.replace(' ', '-')}-{self.type}" + self._attr_unique_id = f"{self._device_name.replace(' ', '-')}-{self.type}" self._model = PUBLIC - @property - def icon(self): - """Icon to use in the frontend.""" - return self._icon - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - @property - def extra_state_attributes(self): - """Return the attributes of the device.""" - attrs = {} - - if self._show_on_map: - attrs[ATTR_LATITUDE] = (self.area.lat_ne + self.area.lat_sw) / 2 - attrs[ATTR_LONGITUDE] = (self.area.lon_ne + self.area.lon_sw) / 2 - - return attrs - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return self._unit_of_measurement - - @property - def available(self): - """Return True if entity is available.""" - return self._state is not None + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: (self.area.lat_ne + self.area.lat_sw) / 2, + ATTR_LONGITUDE: (self.area.lon_ne + self.area.lon_sw) / 2, + } + ) @property def _data(self): @@ -668,18 +614,19 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): data = self._data.get_latest_gust_strengths() if data is None: - if self._state is None: + if self._attr_state is None: return _LOGGER.debug( "No station provides %s data in the area %s", self.type, self._area_name ) - self._state = None + self._attr_state = None return if values := [x for x in data.values() if x is not None]: if self._mode == "avg": - self._state = round(sum(values) / len(values), 1) + self._attr_state = round(sum(values) / len(values), 1) elif self._mode == "max": - self._state = max(values) + self._attr_state = max(values) + self._attr_available = self._attr_state is not None self.async_write_ha_state() From d339e3bd8c0d1030fe1f686af26e9f890f8ff687 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 2 Jul 2021 11:49:42 +0200 Subject: [PATCH 036/818] Reject trusted network access from proxies (#52388) --- .../auth/providers/trusted_networks.py | 14 ++++++++ homeassistant/components/http/forwarded.py | 8 ++--- tests/auth/providers/test_trusted_networks.py | 25 ++++++++++++++ tests/components/http/test_forwarded.py | 33 ++++--------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index fd2014667f8..7b609f371ef 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -81,6 +81,17 @@ class TrustedNetworksAuthProvider(AuthProvider): """Return trusted users per network.""" return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS]) + @property + def trusted_proxies(self) -> list[IPNetwork]: + """Return trusted proxies in the system.""" + if not self.hass.http: + return [] + + return [ + ip_network(trusted_proxy) + for trusted_proxy in self.hass.http.trusted_proxies + ] + @property def support_mfa(self) -> bool: """Trusted Networks auth provider does not support MFA.""" @@ -178,6 +189,9 @@ class TrustedNetworksAuthProvider(AuthProvider): ): raise InvalidAuthError("Not in trusted_networks") + if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies): + raise InvalidAuthError("Can't allow access from a proxy server") + @callback def async_validate_refresh_token( self, refresh_token: RefreshToken, remote_ip: str | None = None diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 684dbbb9e2b..18bc51af1d1 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -129,11 +129,9 @@ def async_setup_forwarded( overrides["remote"] = str(forwarded_ip) break else: - _LOGGER.warning( - "Request originated directly from a trusted proxy included in X-Forwarded-For: %s, this is likely a miss configuration and will be rejected", - forwarded_for_headers, - ) - raise HTTPBadRequest() + # If all the IP addresses are from trusted networks, take the left-most. + forwarded_for_index = -1 + overrides["remote"] = str(forwarded_for[-1]) # Handle X-Forwarded-Proto forwarded_proto_headers: list[str] = request.headers.getall( diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 412f660adc3..d7574bf0da1 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -8,7 +8,9 @@ import voluptuous as vol from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.setup import async_setup_component @pytest.fixture @@ -144,6 +146,29 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) +async def test_validate_access_proxy(hass, provider): + """Test validate access from trusted networks are blocked from proxy.""" + + await async_setup_component( + hass, + "http", + { + "http": { + CONF_TRUSTED_PROXIES: ["192.168.128.0/31", "fd00::1"], + CONF_USE_X_FORWARDED_FOR: True, + } + }, + ) + provider.async_validate_access(ip_address("192.168.128.2")) + provider.async_validate_access(ip_address("fd00::2")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.0")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.1")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("fd00::1")) + + async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 8d8467b699f..400a1f32729 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -43,9 +43,15 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): @pytest.mark.parametrize( "trusted_proxies,x_forwarded_for,remote", [ + ( + ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], + "10.10.10.10, 1.1.1.1", + "10.10.10.10", + ), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123,2.2.2.2,1.1.1.1", "2.2.2.2"), (["127.0.0.0/24"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "1.1.1.1"), + (["127.0.0.0/24"], "127.0.0.1", "127.0.0.1"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 1.1.1.1", "123.123.123.123"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.1"], "255.255.255.255", "255.255.255.255"), @@ -77,33 +83,6 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 -@pytest.mark.parametrize( - "trusted_proxies,x_forwarded_for", - [ - ( - ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], - "10.10.10.10, 1.1.1.1", - ), - (["127.0.0.0/24"], "127.0.0.1"), - ], -) -async def test_x_forwarded_for_from_trusted_proxy_rejected( - trusted_proxies, x_forwarded_for, aiohttp_client -): - """Test that we reject forwarded requests from proxy server itself.""" - - app = web.Application() - app.router.add_get("/", mock_handler) - async_setup_forwarded( - app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] - ) - - mock_api_client = await aiohttp_client(app) - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) - - assert resp.status == 400 - - async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): """Test that we warn when processing is disabled, but proxy has been detected.""" From 24ae05b734296069c92dab7d8df7d010fc8aabbf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 13:17:00 +0200 Subject: [PATCH 037/818] Drop statistic_id and source columns from statistics table (#52417) * Drop statistic_id and source columns from statistics table * Remove useless double drop of statistics table * Update homeassistant/components/recorder/models.py Co-authored-by: Franck Nijhof * black Co-authored-by: Franck Nijhof --- .../components/recorder/migration.py | 16 ++- homeassistant/components/recorder/models.py | 25 ++-- .../components/recorder/statistics.py | 133 ++++++++++-------- homeassistant/components/sensor/recorder.py | 6 +- 4 files changed, 107 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index b219209b386..30a5162e947 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -11,7 +11,14 @@ from sqlalchemy.exc import ( ) from sqlalchemy.schema import AddConstraint, DropConstraint -from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges, Statistics +from .models import ( + SCHEMA_VERSION, + TABLE_STATES, + Base, + SchemaChanges, + Statistics, + StatisticsMeta, +) from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -453,10 +460,15 @@ def _apply_update(engine, session, new_version, old_version): connection, engine, TABLE_STATES, ["old_state_id"] ) elif new_version == 17: + # This dropped the statistics table, done again in version 18. + pass + elif new_version == 18: if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - # Recreate the statistics table + # Recreate the statistics and statisticsmeta tables Statistics.__table__.drop(engine) Statistics.__table__.create(engine) + StatisticsMeta.__table__.drop(engine) + StatisticsMeta.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 5a96cbf4f0b..50052c1f722 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -36,7 +36,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 17 +SCHEMA_VERSION = 18 _LOGGER = logging.getLogger(__name__) @@ -224,8 +224,11 @@ class Statistics(Base): # type: ignore __tablename__ = TABLE_STATISTICS id = Column(Integer, primary_key=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - source = Column(String(32)) - statistic_id = Column(String(255)) + metadata_id = Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) start = Column(DATETIME_TYPE, index=True) mean = Column(Float()) min = Column(Float()) @@ -236,15 +239,14 @@ class Statistics(Base): # type: ignore __table_args__ = ( # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "statistic_id", "start"), + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), ) @staticmethod - def from_stats(source, statistic_id, start, stats): + def from_stats(metadata_id, start, stats): """Create object from a statistics.""" return Statistics( - source=source, - statistic_id=statistic_id, + metadata_id=metadata_id, start=start, **stats, ) @@ -258,17 +260,22 @@ class StatisticsMeta(Base): # type: ignore "mysql_collate": "utf8mb4_unicode_ci", } __tablename__ = TABLE_STATISTICS_META - statistic_id = Column(String(255), primary_key=True) + id = Column(Integer, primary_key=True) + statistic_id = Column(String(255), index=True) source = Column(String(32)) unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) @staticmethod - def from_meta(source, statistic_id, unit_of_measurement): + def from_meta(source, statistic_id, unit_of_measurement, has_mean, has_sum): """Create object from meta data.""" return StatisticsMeta( source=source, statistic_id=statistic_id, unit_of_measurement=unit_of_measurement, + has_mean=has_mean, + has_sum=has_sum, ) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0e01005c13a..e1dd0fb986a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from . import Recorder QUERY_STATISTICS = [ - Statistics.statistic_id, + Statistics.metadata_id, Statistics.start, Statistics.mean, Statistics.min, @@ -33,11 +33,8 @@ QUERY_STATISTICS = [ Statistics.sum, ] -QUERY_STATISTIC_IDS = [ - Statistics.statistic_id, -] - QUERY_STATISTIC_META = [ + StatisticsMeta.id, StatisticsMeta.statistic_id, StatisticsMeta.unit_of_measurement, ] @@ -76,16 +73,39 @@ def get_start_time() -> datetime.datetime: return start +def _get_metadata_ids(hass, session, statistic_ids): + """Resolve metadata_id for a list of statistic_ids.""" + baked_query = hass.data[STATISTICS_META_BAKERY]( + lambda session: session.query(*QUERY_STATISTIC_META) + ) + baked_query += lambda q: q.filter( + StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) + ) + result = execute(baked_query(session).params(statistic_ids=statistic_ids)) + + return [id for id, _, _ in result] + + +def _get_or_add_metadata_id(hass, session, statistic_id, metadata): + """Get metadata_id for a statistic_id, add if it doesn't exist.""" + metadata_id = _get_metadata_ids(hass, session, [statistic_id]) + if not metadata_id: + unit = metadata["unit_of_measurement"] + has_mean = metadata["has_mean"] + has_sum = metadata["has_sum"] + session.add( + StatisticsMeta.from_meta(DOMAIN, statistic_id, unit, has_mean, has_sum) + ) + metadata_id = _get_metadata_ids(hass, session, [statistic_id]) + return metadata_id[0] + + @retryable_database_job("statistics") def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: """Compile statistics.""" start = dt_util.as_utc(start) end = start + timedelta(hours=1) - _LOGGER.debug( - "Compiling statistics for %s-%s", - start, - end, - ) + _LOGGER.debug("Compiling statistics for %s-%s", start, end) platform_stats = [] for domain, platform in instance.hass.data[DOMAIN].items(): if not hasattr(platform, "compile_statistics"): @@ -98,29 +118,22 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: with session_scope(session=instance.get_session()) as session: # type: ignore for stats in platform_stats: for entity_id, stat in stats.items(): - session.add( - Statistics.from_stats(DOMAIN, entity_id, start, stat["stat"]) + metadata_id = _get_or_add_metadata_id( + instance.hass, session, entity_id, stat["meta"] ) - exists = session.query( - session.query(StatisticsMeta) - .filter_by(statistic_id=entity_id) - .exists() - ).scalar() - if not exists: - unit = stat["meta"]["unit_of_measurement"] - session.add(StatisticsMeta.from_meta(DOMAIN, entity_id, unit)) + session.add(Statistics.from_stats(metadata_id, start, stat["stat"])) return True -def _get_meta_data(hass, session, statistic_ids): +def _get_meta_data(hass, session, statistic_ids, statistic_type): """Fetch meta data.""" - def _meta(metas, wanted_statistic_id): - meta = {"statistic_id": wanted_statistic_id, "unit_of_measurement": None} - for statistic_id, unit in metas: - if statistic_id == wanted_statistic_id: - meta["unit_of_measurement"] = unit + def _meta(metas, wanted_metadata_id): + meta = None + for metadata_id, statistic_id, unit in metas: + if metadata_id == wanted_metadata_id: + meta = {"unit_of_measurement": unit, "statistic_id": statistic_id} return meta baked_query = hass.data[STATISTICS_META_BAKERY]( @@ -130,13 +143,14 @@ def _get_meta_data(hass, session, statistic_ids): baked_query += lambda q: q.filter( StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) ) - + if statistic_type == "mean": + baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False)) + if statistic_type == "sum": + baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False)) result = execute(baked_query(session).params(statistic_ids=statistic_ids)) - if statistic_ids is None: - statistic_ids = [statistic_id[0] for statistic_id in result] - - return {id: _meta(result, id) for id in statistic_ids} + metadata_ids = [metadata[0] for metadata in result] + return {id: _meta(result, id) for id in metadata_ids} def _configured_unit(unit: str, units) -> str: @@ -152,24 +166,11 @@ def list_statistic_ids(hass, statistic_type=None): """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: - baked_query = hass.data[STATISTICS_BAKERY]( - lambda session: session.query(*QUERY_STATISTIC_IDS).distinct() - ) + meta_data = _get_meta_data(hass, session, None, statistic_type) - if statistic_type == "mean": - baked_query += lambda q: q.filter(Statistics.mean.isnot(None)) - if statistic_type == "sum": - baked_query += lambda q: q.filter(Statistics.sum.isnot(None)) - - baked_query += lambda q: q.order_by(Statistics.statistic_id) - - result = execute(baked_query(session)) - - statistic_ids = [statistic_id[0] for statistic_id in result] - meta_data = _get_meta_data(hass, session, statistic_ids) - for item in meta_data.values(): - unit = _configured_unit(item["unit_of_measurement"], units) - item["unit_of_measurement"] = unit + for meta in meta_data.values(): + unit = _configured_unit(meta["unit_of_measurement"], units) + meta["unit_of_measurement"] = unit return list(meta_data.values()) @@ -186,20 +187,24 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None if end_time is not None: baked_query += lambda q: q.filter(Statistics.start < bindparam("end_time")) + metadata_ids = None if statistic_ids is not None: baked_query += lambda q: q.filter( - Statistics.statistic_id.in_(bindparam("statistic_ids")) + Statistics.metadata_id.in_(bindparam("metadata_ids")) ) statistic_ids = [statistic_id.lower() for statistic_id in statistic_ids] + metadata_ids = _get_metadata_ids(hass, session, statistic_ids) + if not metadata_ids: + return {} - baked_query += lambda q: q.order_by(Statistics.statistic_id, Statistics.start) + baked_query += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) stats = execute( baked_query(session).params( - start_time=start_time, end_time=end_time, statistic_ids=statistic_ids + start_time=start_time, end_time=end_time, metadata_ids=metadata_ids ) ) - meta_data = _get_meta_data(hass, session, statistic_ids) + meta_data = _get_meta_data(hass, session, statistic_ids, None) return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) @@ -210,23 +215,28 @@ def get_last_statistics(hass, number_of_stats, statistic_id=None): lambda session: session.query(*QUERY_STATISTICS) ) + metadata_id = None if statistic_id is not None: - baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) + baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) + metadata_ids = _get_metadata_ids(hass, session, [statistic_id]) + if not metadata_ids: + return {} + metadata_id = metadata_ids[0] baked_query += lambda q: q.order_by( - Statistics.statistic_id, Statistics.start.desc() + Statistics.metadata_id, Statistics.start.desc() ) baked_query += lambda q: q.limit(bindparam("number_of_stats")) stats = execute( baked_query(session).params( - number_of_stats=number_of_stats, statistic_id=statistic_id + number_of_stats=number_of_stats, metadata_id=metadata_id ) ) statistic_ids = [statistic_id] if statistic_id is not None else None - meta_data = _get_meta_data(hass, session, statistic_ids) + meta_data = _get_meta_data(hass, session, statistic_ids, None) return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) @@ -249,13 +259,14 @@ def _sorted_statistics_to_dict( _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat # Append all statistic entries, and do unit conversion - for ent_id, group in groupby(stats, lambda state: state.statistic_id): - unit = meta_data[ent_id]["unit_of_measurement"] + for meta_id, group in groupby(stats, lambda state: state.metadata_id): + unit = meta_data[meta_id]["unit_of_measurement"] + statistic_id = meta_data[meta_id]["statistic_id"] convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) - ent_results = result[ent_id] + ent_results = result[meta_id] ent_results.extend( { - "statistic_id": db_state.statistic_id, + "statistic_id": statistic_id, "start": _process_timestamp_to_utc_isoformat(db_state.start), "mean": convert(db_state.mean, units), "min": convert(db_state.min, units), @@ -268,4 +279,4 @@ def _sorted_statistics_to_dict( ) # Filter out the empty lists if some states had 0 results. - return {key: val for key, val in result.items() if val} + return {meta_data[key]["statistic_id"]: val for key, val in result.items() if val} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index d9200bdf797..bbd49814076 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -224,7 +224,11 @@ def compile_statistics( result[entity_id] = {} # Set meta data - result[entity_id]["meta"] = {"unit_of_measurement": unit} + result[entity_id]["meta"] = { + "unit_of_measurement": unit, + "has_mean": "mean" in wanted_statistics, + "has_sum": "sum" in wanted_statistics, + } # Make calculations stat: dict = {} From dec08e2e446fafb016c1a61406eefa4f069dc0c0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 14:10:32 +0200 Subject: [PATCH 038/818] Upgrade aioimaplib to 0.9.0 (#52422) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 5bb1efa0ca1..c1823459745 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==0.7.15"], + "requirements": ["aioimaplib==0.9.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 3a7808389df..714b7495a18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aiohttp_cors==0.7.0 aiohue==2.5.1 # homeassistant.components.imap -aioimaplib==0.7.15 +aioimaplib==0.9.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 7b940d23821abdc7208a1c132a4a0380e3524d54 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 2 Jul 2021 15:09:36 +0200 Subject: [PATCH 039/818] Fix typo in forecast_solar strings (#52430) --- homeassistant/components/forecast_solar/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index eb98fc79297..e1ae451a04f 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -17,7 +17,7 @@ "options": { "step": { "init": { - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation is a field is unclear.", + "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear.", "data": { "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", From 11fd9d95253aabcf8f25543e567e63382685f61b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 15:40:54 +0200 Subject: [PATCH 040/818] Avoid duplicated database queries when fetching statistics (#52433) --- .../components/recorder/statistics.py | 51 +++++----- tests/components/recorder/test_statistics.py | 97 +++++++++++++++---- 2 files changed, 102 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e1dd0fb986a..2ef49df7ded 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -126,7 +126,7 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: return True -def _get_meta_data(hass, session, statistic_ids, statistic_type): +def _get_metadata(hass, session, statistic_ids, statistic_type): """Fetch meta data.""" def _meta(metas, wanted_metadata_id): @@ -166,18 +166,23 @@ def list_statistic_ids(hass, statistic_type=None): """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: - meta_data = _get_meta_data(hass, session, None, statistic_type) + metadata = _get_metadata(hass, session, None, statistic_type) - for meta in meta_data.values(): + for meta in metadata.values(): unit = _configured_unit(meta["unit_of_measurement"], units) meta["unit_of_measurement"] = unit - return list(meta_data.values()) + return list(metadata.values()) def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): """Return states changes during UTC period start_time - end_time.""" + metadata = None with session_scope(hass=hass) as session: + metadata = _get_metadata(hass, session, statistic_ids, None) + if not metadata: + return {} + baked_query = hass.data[STATISTICS_BAKERY]( lambda session: session.query(*QUERY_STATISTICS) ) @@ -192,10 +197,7 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None baked_query += lambda q: q.filter( Statistics.metadata_id.in_(bindparam("metadata_ids")) ) - statistic_ids = [statistic_id.lower() for statistic_id in statistic_ids] - metadata_ids = _get_metadata_ids(hass, session, statistic_ids) - if not metadata_ids: - return {} + metadata_ids = list(metadata.keys()) baked_query += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) @@ -204,24 +206,23 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None start_time=start_time, end_time=end_time, metadata_ids=metadata_ids ) ) - meta_data = _get_meta_data(hass, session, statistic_ids, None) - return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) + return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata) -def get_last_statistics(hass, number_of_stats, statistic_id=None): - """Return the last number_of_stats statistics.""" +def get_last_statistics(hass, number_of_stats, statistic_id): + """Return the last number_of_stats statistics for a statistic_id.""" + statistic_ids = [statistic_id] with session_scope(hass=hass) as session: + metadata = _get_metadata(hass, session, statistic_ids, None) + if not metadata: + return {} + baked_query = hass.data[STATISTICS_BAKERY]( lambda session: session.query(*QUERY_STATISTICS) ) - metadata_id = None - if statistic_id is not None: - baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) - metadata_ids = _get_metadata_ids(hass, session, [statistic_id]) - if not metadata_ids: - return {} - metadata_id = metadata_ids[0] + baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) + metadata_id = next(iter(metadata.keys())) baked_query += lambda q: q.order_by( Statistics.metadata_id, Statistics.start.desc() @@ -235,16 +236,14 @@ def get_last_statistics(hass, number_of_stats, statistic_id=None): ) ) - statistic_ids = [statistic_id] if statistic_id is not None else None - meta_data = _get_meta_data(hass, session, statistic_ids, None) - return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) + return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata) def _sorted_statistics_to_dict( hass, stats, statistic_ids, - meta_data, + metadata, ): """Convert SQL results into JSON friendly data structure.""" result = defaultdict(list) @@ -260,8 +259,8 @@ def _sorted_statistics_to_dict( # Append all statistic entries, and do unit conversion for meta_id, group in groupby(stats, lambda state: state.metadata_id): - unit = meta_data[meta_id]["unit_of_measurement"] - statistic_id = meta_data[meta_id]["statistic_id"] + unit = metadata[meta_id]["unit_of_measurement"] + statistic_id = metadata[meta_id]["statistic_id"] convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) ent_results = result[meta_id] ent_results.extend( @@ -279,4 +278,4 @@ def _sorted_statistics_to_dict( ) # Filter out the empty lists if some states had 0 results. - return {meta_data[key]["statistic_id"]: val for key, val in result.items() if val} + return {metadata[key]["statistic_id"]: val for key, val in result.items() if val} diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 104617aee2c..32eaaaab842 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -8,7 +8,10 @@ from pytest import approx from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat -from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.components.recorder.statistics import ( + get_last_statistics, + statistics_during_period, +) from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -25,24 +28,69 @@ def test_compile_hourly_statistics(hass_recorder): hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(period="hourly", start=zero) - wait_recording_done(hass) for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, **kwargs) - assert stats == { - "sensor.test1": [ - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "mean": approx(14.915254237288135), - "min": approx(10.0), - "max": approx(20.0), - "last_reset": None, - "state": None, - "sum": None, - } - ] - } + assert stats == {} + stats = get_last_statistics(hass, 0, "sensor.test1") + assert stats == {} + + recorder.do_adhoc_statistics(period="hourly", start=zero) + recorder.do_adhoc_statistics(period="hourly", start=four) + wait_recording_done(hass) + expected_1 = { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_2 = { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(four), + "mean": approx(20.0), + "min": approx(20.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_stats1 = [ + {**expected_1, "statistic_id": "sensor.test1"}, + {**expected_2, "statistic_id": "sensor.test1"}, + ] + expected_stats2 = [ + {**expected_1, "statistic_id": "sensor.test2"}, + {**expected_2, "statistic_id": "sensor.test2"}, + ] + + # Test statistics_during_period + stats = statistics_during_period(hass, zero) + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + stats = statistics_during_period(hass, zero, statistic_ids=["sensor.test2"]) + assert stats == {"sensor.test2": expected_stats2} + + stats = statistics_during_period(hass, zero, statistic_ids=["sensor.test3"]) + assert stats == {} + + # Test get_last_statistics + stats = get_last_statistics(hass, 0, "sensor.test1") + assert stats == {} + + stats = get_last_statistics(hass, 1, "sensor.test1") + assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} + + stats = get_last_statistics(hass, 2, "sensor.test1") + assert stats == {"sensor.test1": expected_stats1[::-1]} + + stats = get_last_statistics(hass, 3, "sensor.test1") + assert stats == {"sensor.test1": expected_stats1[::-1]} + + stats = get_last_statistics(hass, 1, "sensor.test3") + assert stats == {} def record_states(hass): @@ -54,13 +102,19 @@ def record_states(hass): sns1 = "sensor.test1" sns2 = "sensor.test2" sns3 = "sensor.test3" + sns4 = "sensor.test4" sns1_attr = { "device_class": "temperature", "state_class": "measurement", "unit_of_measurement": TEMP_CELSIUS, } - sns2_attr = {"device_class": "temperature"} - sns3_attr = {} + sns2_attr = { + "device_class": "humidity", + "state_class": "measurement", + "unit_of_measurement": "%", + } + sns3_attr = {"device_class": "temperature"} + sns4_attr = {} def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -74,7 +128,7 @@ def record_states(hass): three = two + timedelta(minutes=30) four = three + timedelta(minutes=15) - states = {mp: [], sns1: [], sns2: [], sns3: []} + states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): states[mp].append( set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) @@ -85,15 +139,18 @@ def record_states(hass): states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + states[sns4].append(set_state(sns4, "15", attributes=sns4_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + states[sns4].append(set_state(sns4, "20", attributes=sns4_attr)) return zero, four, states From ed25e6fadab38793a55d4c7bc09c21fc3b9e02a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 16:28:16 +0200 Subject: [PATCH 041/818] Correct recorder table arguments (#52436) --- homeassistant/components/recorder/models.py | 52 +++++++-------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 50052c1f722..c77d824c64f 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -66,10 +66,12 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant( class Events(Base): # type: ignore """Event history data.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) __tablename__ = TABLE_EVENTS event_id = Column(Integer, Identity(), primary_key=True) event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) @@ -81,12 +83,6 @@ class Events(Base): # type: ignore context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - __table_args__ = ( - # Used for fetching events at a specific time - # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), - ) - def __repr__(self) -> str: """Return string representation of instance for debugging.""" return ( @@ -133,10 +129,12 @@ class Events(Base): # type: ignore class States(Base): # type: ignore """State change history.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) __tablename__ = TABLE_STATES state_id = Column(Integer, Identity(), primary_key=True) domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) @@ -153,12 +151,6 @@ class States(Base): # type: ignore event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) - __table_args__ = ( - # Used for fetching the state of entities at a specific time - # (get_states in history.py) - Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), - ) - def __repr__(self) -> str: """Return string representation of instance for debugging.""" return ( @@ -217,10 +209,10 @@ class States(Base): # type: ignore class Statistics(Base): # type: ignore """Statistics.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + ) __tablename__ = TABLE_STATISTICS id = Column(Integer, primary_key=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) @@ -237,11 +229,6 @@ class Statistics(Base): # type: ignore state = Column(Float()) sum = Column(Float()) - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "metadata_id", "start"), - ) - @staticmethod def from_stats(metadata_id, start, stats): """Create object from a statistics.""" @@ -255,10 +242,6 @@ class Statistics(Base): # type: ignore class StatisticsMeta(Base): # type: ignore """Statistics meta data.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } __tablename__ = TABLE_STATISTICS_META id = Column(Integer, primary_key=True) statistic_id = Column(String(255), index=True) @@ -282,6 +265,7 @@ class StatisticsMeta(Base): # type: ignore class RecorderRuns(Base): # type: ignore """Representation of recorder run.""" + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) __tablename__ = TABLE_RECORDER_RUNS run_id = Column(Integer, Identity(), primary_key=True) start = Column(DateTime(timezone=True), default=dt_util.utcnow) @@ -289,8 +273,6 @@ class RecorderRuns(Base): # type: ignore closed_incorrect = Column(Boolean, default=False) created = Column(DateTime(timezone=True), default=dt_util.utcnow) - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) - def __repr__(self) -> str: """Return string representation of instance for debugging.""" end = ( From 98fdb00bc731d909c94afc8ef49f304bd11458ae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 16:45:38 +0200 Subject: [PATCH 042/818] Enable basic type checking for Tasmota (#52435) --- homeassistant/components/tasmota/device_trigger.py | 6 +++--- homeassistant/components/tasmota/discovery.py | 4 ++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 6a6f0324e1d..8ec368e326f 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -81,9 +81,9 @@ class Trigger: """Device trigger settings.""" device_id: str = attr.ib() - discovery_hash: dict = attr.ib() + discovery_hash: dict | None = attr.ib() hass: HomeAssistant = attr.ib() - remove_update_signal: Callable[[], None] = attr.ib() + remove_update_signal: Callable[[], None] | None = attr.ib() subtype: str = attr.ib() tasmota_trigger: TasmotaTrigger = attr.ib() type: str = attr.ib() @@ -242,7 +242,7 @@ async def async_remove_triggers(hass: HomeAssistant, device_id: str): async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for a Tasmota device.""" - triggers = [] + triggers: list[dict[str, str]] = [] if DEVICE_TRIGGERS not in hass.data: return triggers diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index b69307693da..ad3604c06c2 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -42,7 +42,7 @@ def set_discovery_hash(hass, discovery_hash): async def async_start( hass: HomeAssistant, discovery_topic, config_entry, tasmota_mqtt, setup_device -) -> bool: +) -> None: """Start Tasmota device discovery.""" async def _discover_entity(tasmota_entity_config, discovery_hash, platform): @@ -171,7 +171,7 @@ async def async_start( hass.data[TASMOTA_DISCOVERY_INSTANCE] = tasmota_discovery -async def async_stop(hass: HomeAssistant) -> bool: +async def async_stop(hass: HomeAssistant) -> None: """Stop Tasmota device discovery.""" hass.data.pop(ALREADY_DISCOVERED) tasmota_discovery = hass.data.pop(TASMOTA_DISCOVERY_INSTANCE) diff --git a/mypy.ini b/mypy.ini index 4472311279f..5f2eeb11c7d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1534,9 +1534,6 @@ ignore_errors = true [mypy-homeassistant.components.tado.*] ignore_errors = true -[mypy-homeassistant.components.tasmota.*] -ignore_errors = true - [mypy-homeassistant.components.telegram_bot.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 601e6b55845..e59a59de7c9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -199,7 +199,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.system_health.*", "homeassistant.components.system_log.*", "homeassistant.components.tado.*", - "homeassistant.components.tasmota.*", "homeassistant.components.telegram_bot.*", "homeassistant.components.template.*", "homeassistant.components.tesla.*", From a3f148978519cb5760a1958f81361930768b0be7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Jul 2021 10:24:43 -0500 Subject: [PATCH 043/818] Import track_new_devices and scan_interval from yaml for nmap_tracker (#52409) * Import track_new_devices and scan_interval from yaml for nmap_tracker * Import track_new_devices and scan_interval from yaml for nmap_tracker * Import track_new_devices and scan_interval from yaml for nmap_tracker * tests * translate * tweak * adjust * save indent * pylint * There are two CONF_SCAN_INTERVAL constants * adjust name -- there are TWO CONF_SCAN_INTERVAL constants * remove CONF_SCAN_INTERVAL/CONF_TRACK_NEW from user flow * assert it does not appear in the user step --- .../components/nmap_tracker/__init__.py | 65 ++++++++++++------- .../components/nmap_tracker/config_flow.py | 55 +++++++++++----- .../components/nmap_tracker/const.py | 2 + .../components/nmap_tracker/device_tracker.py | 37 +++++++++-- .../components/nmap_tracker/strings.json | 6 +- .../nmap_tracker/translations/en.json | 4 +- .../nmap_tracker/test_config_flow.py | 25 +++++++ 7 files changed, 146 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 381813a3b49..399121e4e00 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -12,6 +12,10 @@ from getmac import get_mac_address from mac_vendor_lookup import AsyncMacLookup from nmap import PortScanner, PortScannerError +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, HomeAssistant, callback @@ -25,6 +29,7 @@ import homeassistant.util.dt as dt_util from .const import ( CONF_HOME_INTERVAL, CONF_OPTIONS, + DEFAULT_TRACK_NEW_DEVICES, DOMAIN, NMAP_TRACKED_DEVICES, PLATFORMS, @@ -146,7 +151,10 @@ class NmapDeviceScanner: self._hosts = None self._options = None self._exclude = None + self._scan_interval = None + self._track_new_devices = None + self._known_mac_addresses = {} self._finished_first_scan = False self._last_results = [] self._mac_vendor_lookup = None @@ -154,6 +162,10 @@ class NmapDeviceScanner: async def async_setup(self): """Set up the tracker.""" config = self._entry.options + self._track_new_devices = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES) + self._scan_interval = timedelta( + seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) + ) self._hosts = cv.ensure_list_csv(config[CONF_HOSTS]) self._exclude = cv.ensure_list_csv(config[CONF_EXCLUDE]) self._options = config[CONF_OPTIONS] @@ -170,6 +182,12 @@ class NmapDeviceScanner: EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner ) ) + registry = er.async_get(self._hass) + self._known_mac_addresses = { + entry.unique_id: entry.original_name + for entry in registry.entities.values() + if entry.config_entry_id == self._entry_id + } @property def signal_device_new(self) -> str: @@ -199,7 +217,7 @@ class NmapDeviceScanner: async_track_time_interval( self._hass, self._async_scan_devices, - timedelta(seconds=TRACKER_SCAN_INTERVAL), + self._scan_interval, ) ) self._mac_vendor_lookup = AsyncMacLookup() @@ -258,26 +276,22 @@ class NmapDeviceScanner: # After all config entries have finished their first # scan we mark devices that were not found as not_home # from unavailable - registry = er.async_get(self._hass) now = dt_util.now() - for entry in registry.entities.values(): - if entry.config_entry_id != self._entry_id: + for mac_address, original_name in self._known_mac_addresses.items(): + if mac_address in self.devices.tracked: continue - if entry.unique_id not in self.devices.tracked: - self.devices.config_entry_owner[entry.unique_id] = self._entry_id - self.devices.tracked[entry.unique_id] = NmapDevice( - entry.unique_id, - None, - entry.original_name, - None, - self._async_get_vendor(entry.unique_id), - "Device not found in initial scan", - now, - 1, - ) - async_dispatcher_send( - self._hass, self.signal_device_missing, entry.unique_id - ) + self.devices.config_entry_owner[mac_address] = self._entry_id + self.devices.tracked[mac_address] = NmapDevice( + mac_address, + None, + original_name, + None, + self._async_get_vendor(mac_address), + "Device not found in initial scan", + now, + 1, + ) + async_dispatcher_send(self._hass, self.signal_device_missing, mac_address) def _run_nmap_scan(self): """Run nmap and return the result.""" @@ -344,21 +358,28 @@ class NmapDeviceScanner: _LOGGER.info("No MAC address found for %s", ipv4) continue - hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - formatted_mac = format_mac(mac) + new = formatted_mac not in devices.tracked + if ( + new + and not self._track_new_devices + and formatted_mac not in devices.tracked + and formatted_mac not in self._known_mac_addresses + ): + continue + if ( devices.config_entry_owner.setdefault(formatted_mac, entry_id) != entry_id ): continue + hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) name = human_readable_name(hostname, vendor, mac) device = NmapDevice( formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 ) - new = formatted_mac not in devices.tracked devices.tracked[formatted_mac] = device devices.ipv4_last_mac[ipv4] = formatted_mac diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index 942689ad575..68e61745b63 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -8,13 +8,24 @@ import ifaddr import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DEFAULT_TRACK_NEW_DEVICES, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) DEFAULT_NETWORK_PREFIX = 24 @@ -92,23 +103,35 @@ def normalize_input(user_input): return errors -async def _async_build_schema_with_user_input(hass, user_input): +async def _async_build_schema_with_user_input(hass, user_input, include_options): hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) exclude = user_input.get( CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) ) - return vol.Schema( - { - vol.Required(CONF_HOSTS, default=hosts): str, - vol.Required( - CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) - ): int, - vol.Optional(CONF_EXCLUDE, default=exclude): str, - vol.Optional( - CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) - ): str, - } - ) + schema = { + vol.Required(CONF_HOSTS, default=hosts): str, + vol.Required( + CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) + ): int, + vol.Optional(CONF_EXCLUDE, default=exclude): str, + vol.Optional( + CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) + ): str, + } + if include_options: + schema.update( + { + vol.Optional( + CONF_TRACK_NEW, + default=user_input.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES), + ): bool, + vol.Optional( + CONF_SCAN_INTERVAL, + default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL), + ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), + } + ) + return vol.Schema(schema) class OptionsFlowHandler(config_entries.OptionsFlow): @@ -133,7 +156,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form( step_id="init", data_schema=await _async_build_schema_with_user_input( - self.hass, self.options + self.hass, self.options, True ), errors=errors, ) @@ -170,7 +193,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=await _async_build_schema_with_user_input( - self.hass, self.options + self.hass, self.options, False ), errors=errors, ) diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index e71c2d58bbb..88118a81811 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -12,3 +12,5 @@ CONF_OPTIONS = "scan_options" DEFAULT_OPTIONS = "-F --host-timeout 5s" TRACKER_SCAN_INTERVAL = 120 + +DEFAULT_TRACK_NEW_DEVICES = True diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 24e0d3d8e26..350e75adf48 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -11,6 +11,11 @@ from homeassistant.components.device_tracker import ( SOURCE_TYPE_ROUTER, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import ( + CONF_NEW_DEVICE_DEFAULTS, + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback @@ -19,7 +24,14 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import NmapDeviceScanner, short_hostname, signal_device_update -from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DEFAULT_TRACK_NEW_DEVICES, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) _LOGGER = logging.getLogger(__name__) @@ -37,16 +49,27 @@ async def async_get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" validated_config = config[DEVICE_TRACKER_DOMAIN] + if CONF_SCAN_INTERVAL in validated_config: + scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds() + else: + scan_interval = TRACKER_SCAN_INTERVAL + + import_config = { + CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), + CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], + CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), + CONF_OPTIONS: validated_config[CONF_OPTIONS], + CONF_SCAN_INTERVAL: scan_interval, + CONF_TRACK_NEW: validated_config.get(CONF_NEW_DEVICE_DEFAULTS, {}).get( + CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES + ), + } + hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data={ - CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), - CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], - CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), - CONF_OPTIONS: validated_config[CONF_OPTIONS], - }, + data=import_config, ) ) diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index a1e04b681cd..ecb470a6f0d 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -8,8 +8,10 @@ "hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]", "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", - "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]" - } + "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]", + "track_new_devices": "Track new devices", + "interval_seconds": "Scan interval" + } } }, "error": { diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index ed37a6a5410..6b83532a0e2 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -28,7 +28,9 @@ "exclude": "Network addresses (comma seperated) to exclude from scanning", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", "hosts": "Network addresses (comma seperated) to scan", - "scan_options": "Raw configurable scan options for Nmap" + "interval_seconds": "Scan interval", + "scan_options": "Raw configurable scan options for Nmap", + "track_new_devices": "Track new devices" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 1556dee58d9..c4e82936b88 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -4,6 +4,10 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.components.nmap_tracker.const import ( CONF_HOME_INTERVAL, CONF_OPTIONS, @@ -28,6 +32,10 @@ async def test_form(hass: HomeAssistant, hosts: str) -> None: assert result["type"] == "form" assert result["errors"] == {} + schema_defaults = result["data_schema"]({}) + assert CONF_TRACK_NEW not in schema_defaults + assert CONF_SCAN_INTERVAL not in schema_defaults + with patch( "homeassistant.components.nmap_tracker.async_setup_entry", return_value=True, @@ -198,6 +206,15 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + CONF_EXCLUDE: "4.4.4.4", + CONF_HOME_INTERVAL: 3, + CONF_HOSTS: "192.168.1.0/24", + CONF_SCAN_INTERVAL: 120, + CONF_OPTIONS: "-F --host-timeout 5s", + CONF_TRACK_NEW: True, + } + with patch( "homeassistant.components.nmap_tracker.async_setup_entry", return_value=True, @@ -209,6 +226,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 5, CONF_OPTIONS: "-sn", CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", + CONF_SCAN_INTERVAL: 10, + CONF_TRACK_NEW: False, }, ) await hass.async_block_till_done() @@ -219,6 +238,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 5, CONF_OPTIONS: "-sn", CONF_EXCLUDE: "4.4.4.4,5.5.5.5", + CONF_SCAN_INTERVAL: 10, + CONF_TRACK_NEW: False, } assert len(mock_setup_entry.mock_calls) == 1 @@ -238,6 +259,8 @@ async def test_import(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 3, CONF_OPTIONS: DEFAULT_OPTIONS, CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + CONF_TRACK_NEW: False, }, ) await hass.async_block_till_done() @@ -250,6 +273,8 @@ async def test_import(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 3, CONF_OPTIONS: DEFAULT_OPTIONS, CONF_EXCLUDE: "4.4.4.4,6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + CONF_TRACK_NEW: False, } assert len(mock_setup_entry.mock_calls) == 1 From 5cd4471c67a0a359f763e355c508e6317e785598 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 2 Jul 2021 11:43:37 -0400 Subject: [PATCH 044/818] Add sensor platform to goalzero (#49835) * Add sensor platform to goalzero * Tweak * Remove unused DATA_SCHEMA * Simplify * Remove last_reset * Update on reload --- .coveragerc | 1 + homeassistant/components/goalzero/__init__.py | 5 +- .../components/goalzero/binary_sensor.py | 2 +- .../components/goalzero/config_flow.py | 2 - homeassistant/components/goalzero/const.py | 110 ++++++++++++++++++ homeassistant/components/goalzero/sensor.py | 59 ++++++++++ 6 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/goalzero/sensor.py diff --git a/.coveragerc b/.coveragerc index eb0fbdc1fcf..cbdcc60b421 100644 --- a/.coveragerc +++ b/.coveragerc @@ -372,6 +372,7 @@ omit = homeassistant/components/goalfeed/* homeassistant/components/goalzero/__init__.py homeassistant/components/goalzero/binary_sensor.py + homeassistant/components/goalzero/sensor.py homeassistant/components/goalzero/switch.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 8838d3f20fa..f3889d1d385 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -4,6 +4,7 @@ import logging from goalzero import Yeti, exceptions from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR +from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME @@ -21,7 +22,7 @@ from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, MIN_TIME_BETWEEN_ _LOGGER = logging.getLogger(__name__) -PLATFORMS = [DOMAIN_BINARY_SENSOR, DOMAIN_SWITCH] +PLATFORMS = [DOMAIN_BINARY_SENSOR, DOMAIN_SENSOR, DOMAIN_SWITCH] async def async_setup_entry(hass, entry): @@ -42,7 +43,7 @@ async def async_setup_entry(hass, entry): try: await api.get_state() except exceptions.ConnectError as err: - raise UpdateFailed(f"Failed to communicating with device {err}") from err + raise UpdateFailed("Failed to communicate with device") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 59a8a6b3443..aec9fdb0354 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -22,7 +22,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) for sensor_name in BINARY_SENSOR_DICT ] - async_add_entities(sensors, True) + async_add_entities(sensors) class YetiBinarySensor(YetiEntity, BinarySensorEntity): diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index cea47c967a8..50450f95a69 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -18,8 +18,6 @@ from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({vol.Required("host"): str, vol.Required("name"): str}) - class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Goal Zero Yeti.""" diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index 826c2621e23..643c632a352 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -6,7 +6,37 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_POWER, ) +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, + ELECTRICAL_CURRENT_AMPERE, + ENERGY_WATT_HOUR, + PERCENTAGE, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + TEMP_CELSIUS, + TIME_MINUTES, + TIME_SECONDS, + VOLT, +) +ATTR_DEFAULT_ENABLED = "default_enabled" + +CONF_IDENTIFIERS = "identifiers" +CONF_MANUFACTURER = "manufacturer" +CONF_MODEL = "model" +CONF_SW_VERSION = "sw_version" DATA_KEY_COORDINATOR = "coordinator" DOMAIN = "goalzero" DEFAULT_NAME = "Yeti" @@ -25,6 +55,86 @@ BINARY_SENSOR_DICT = { "inputDetected": ["Input Detected", DEVICE_CLASS_POWER, None], } +SENSOR_DICT = { + "wattsIn": { + ATTR_NAME: "Watts In", + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEFAULT_ENABLED: True, + }, + "ampsIn": { + ATTR_NAME: "Amps In", + ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + ATTR_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEFAULT_ENABLED: False, + }, + "wattsOut": { + ATTR_NAME: "Watts Out", + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEFAULT_ENABLED: True, + }, + "ampsOut": { + ATTR_NAME: "Amps Out", + ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + ATTR_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEFAULT_ENABLED: False, + }, + "whOut": { + ATTR_NAME: "WH Out", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEFAULT_ENABLED: False, + }, + "whStored": { + ATTR_NAME: "WH Stored", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEFAULT_ENABLED: True, + }, + "volts": { + ATTR_NAME: "Volts", + ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + ATTR_UNIT_OF_MEASUREMENT: VOLT, + ATTR_DEFAULT_ENABLED: False, + }, + "socPercent": { + ATTR_NAME: "State of Charge Percent", + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEFAULT_ENABLED: True, + }, + "timeToEmptyFull": { + ATTR_NAME: "Time to Empty/Full", + ATTR_DEVICE_CLASS: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_DEFAULT_ENABLED: True, + }, + "temperature": { + ATTR_NAME: "Temperature", + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEFAULT_ENABLED: True, + }, + "wifiStrength": { + ATTR_NAME: "Wifi Strength", + ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + ATTR_DEFAULT_ENABLED: True, + }, + "timestamp": { + ATTR_NAME: "Up Time", + ATTR_UNIT_OF_MEASUREMENT: TIME_SECONDS, + ATTR_DEFAULT_ENABLED: False, + }, +} + SWITCH_DICT = { "v12PortStatus": "12V Port Status", "usbPortStatus": "USB Port Status", diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py new file mode 100644 index 00000000000..8464104a61f --- /dev/null +++ b/homeassistant/components/goalzero/sensor.py @@ -0,0 +1,59 @@ +"""Support for Goal Zero Yeti Sensors.""" +from homeassistant.components.sensor import ATTR_LAST_RESET, ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, +) + +from . import YetiEntity +from .const import ( + ATTR_DEFAULT_ENABLED, + DATA_KEY_API, + DATA_KEY_COORDINATOR, + DOMAIN, + SENSOR_DICT, +) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Goal Zero Yeti sensor.""" + name = entry.data[CONF_NAME] + goalzero_data = hass.data[DOMAIN][entry.entry_id] + sensors = [ + YetiSensor( + goalzero_data[DATA_KEY_API], + goalzero_data[DATA_KEY_COORDINATOR], + name, + sensor_name, + entry.entry_id, + ) + for sensor_name in SENSOR_DICT + ] + async_add_entities(sensors, True) + + +class YetiSensor(YetiEntity): + """Representation of a Goal Zero Yeti sensor.""" + + def __init__(self, api, coordinator, name, sensor_name, server_unique_id): + """Initialize a Goal Zero Yeti sensor.""" + super().__init__(api, coordinator, name, server_unique_id) + + self._condition = sensor_name + + sensor = SENSOR_DICT[sensor_name] + self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" + self._attr_unique_id = f"{self._server_unique_id}/{sensor_name}" + self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) + self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) + self._device_class = sensor.get(ATTR_DEVICE_CLASS) + self._attr_last_reset = sensor.get(ATTR_LAST_RESET) + self._attr_state_class = sensor.get(ATTR_STATE_CLASS) + + @property + def state(self): + """Return the state.""" + if self.api.data: + return self.api.data[self._condition] From 1eb27f7cccb1b517bcb56a0cc8f1227b4dac7ac2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 Jul 2021 18:50:57 +0300 Subject: [PATCH 045/818] Demo: remove deprecated switch entity properties (#52424) * Demo: deprecate switch entity properties * Fix emulated_kasa test --- homeassistant/components/demo/sensor.py | 22 ++++++ homeassistant/components/demo/switch.py | 3 - tests/components/demo/test_switch.py | 88 +++++++++++++++++++++ tests/components/emulated_kasa/test_init.py | 6 ++ 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 tests/components/demo/test_switch.py diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 817cbd435d1..488c34be983 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -10,9 +10,13 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -67,6 +71,24 @@ async def async_setup_platform( CONCENTRATION_PARTS_PER_MILLION, 14, ), + DemoSensor( + "sensor_5", + "Power consumption", + 100, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + POWER_WATT, + None, + ), + DemoSensor( + "sensor_6", + "Today energy", + 15, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + ENERGY_KILO_WATT_HOUR, + None, + ), ] ) diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 13853959a12..84554bf0db1 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -49,7 +49,6 @@ class DemoSwitch(SwitchEntity): self._attr_icon = icon self._attr_is_on = state self._attr_name = name or DEVICE_DEFAULT_NAME - self._attr_today_energy_kwh = 15 self._attr_unique_id = unique_id @property @@ -63,11 +62,9 @@ class DemoSwitch(SwitchEntity): def turn_on(self, **kwargs): """Turn the switch on.""" self._attr_is_on = True - self._attr_current_power_w = 100 self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" self._attr_is_on = False - self._attr_current_power_w = 0 self.schedule_update_ha_state() diff --git a/tests/components/demo/test_switch.py b/tests/components/demo/test_switch.py new file mode 100644 index 00000000000..f35bc14db34 --- /dev/null +++ b/tests/components/demo/test_switch.py @@ -0,0 +1,88 @@ +"""The tests for the demo switch component.""" +import pytest + +from homeassistant.components.demo import DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +SWITCH_ENTITY_IDS = ["switch.decorative_lights", "switch.ac"] + + +@pytest.fixture(autouse=True) +async def setup_comp(hass): + """Set up demo component.""" + assert await async_setup_component( + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": DOMAIN}} + ) + await hass.async_block_till_done() + + +@pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) +async def test_turn_on(hass, switch_entity_id): + """Test switch turn on method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: switch_entity_id}, + blocking=True, + ) + + state = hass.states.get(switch_entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch_entity_id}, + blocking=True, + ) + + state = hass.states.get(switch_entity_id) + assert state.state == STATE_ON + + +@pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) +async def test_turn_off(hass, switch_entity_id): + """Test switch turn off method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch_entity_id}, + blocking=True, + ) + + state = hass.states.get(switch_entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: switch_entity_id}, + blocking=True, + ) + + state = hass.states.get(switch_entity_id) + assert state.state == STATE_OFF + + +@pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) +async def test_turn_off_without_entity_id(hass, switch_entity_id): + """Test switch turn off all switches.""" + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "all"}, blocking=True + ) + + state = hass.states.get(switch_entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "all"}, blocking=True + ) + + state = hass.states.get(switch_entity_id) + assert state.state == STATE_OFF diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index 60f4f5be1db..d68221d84fd 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -217,6 +217,12 @@ async def test_switch_power(hass): SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True ) + hass.states.async_set( + ENTITY_SWITCH, + STATE_ON, + attributes={ATTR_CURRENT_POWER_W: 100, ATTR_FRIENDLY_NAME: "AC"}, + ) + switch = hass.states.get(ENTITY_SWITCH) assert switch.state == STATE_ON power = switch.attributes[ATTR_CURRENT_POWER_W] From e435ac6fcd6ccf64a78f34c01eb6021bdcdddc72 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 2 Jul 2021 11:14:29 -0500 Subject: [PATCH 046/818] Remove redundant property definitions in ReCollect Waste (#52368) * Remove redundant property definitions in ReCollect Waste * Code review * Code review --- .../components/recollect_waste/__init__.py | 8 +--- .../components/recollect_waste/sensor.py | 39 +++++-------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index f061532c3d1..1ef05dbeae9 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -21,14 +21,10 @@ DEFAULT_UPDATE_INTERVAL = timedelta(days=1) PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the RainMachine component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) + session = aiohttp_client.async_get_clientsession(hass) client = Client( entry.data[CONF_PLACE_ID], entry.data[CONF_SERVICE_ID], session=session diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 68c810bc90d..08fbd449a1b 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -84,37 +84,18 @@ async def async_setup_entry( class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_name = DEFAULT_NAME + self._attr_unique_id = ( + f"{entry.data[CONF_PLACE_ID]}{entry.data[CONF_SERVICE_ID]}" + ) self._entry = entry - self._state = None - - @property - def device_class(self) -> dict: - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def extra_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attributes - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return DEFAULT_NAME - - @property - def state(self) -> str: - """Return the state of the sensor.""" - return self._state - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._entry.data[CONF_PLACE_ID]}{self._entry.data[CONF_SERVICE_ID]}" @callback def _handle_coordinator_update(self) -> None: @@ -133,8 +114,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): pickup_event = self.coordinator.data[0] next_pickup_event = self.coordinator.data[1] - self._state = as_utc(pickup_event.date).isoformat() - self._attributes.update( + self._attr_extra_state_attributes.update( { ATTR_PICKUP_TYPES: async_get_pickup_type_names( self._entry, pickup_event.pickup_types @@ -146,3 +126,4 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(), } ) + self._attr_state = as_utc(pickup_event.date).isoformat() From 8c7ef5b1b9d52da1fe8a8924792eca998ba3e5d7 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 2 Jul 2021 18:37:18 +0200 Subject: [PATCH 047/818] Add static typing to devolo_home_control (#52396) --- .strict-typing | 1 + .../devolo_home_control/__init__.py | 12 ++++-- .../devolo_home_control/binary_sensor.py | 32 ++++++++++----- .../components/devolo_home_control/climate.py | 15 ++++--- .../devolo_home_control/config_flow.py | 34 +++++++++++----- .../components/devolo_home_control/cover.py | 21 ++++++---- .../devolo_home_control/devolo_device.py | 37 ++++++++++------- .../devolo_multi_level_switch.py | 7 +++- .../components/devolo_home_control/light.py | 24 +++++++---- .../components/devolo_home_control/sensor.py | 40 +++++++++++++------ .../devolo_home_control/subscriber.py | 5 ++- .../components/devolo_home_control/switch.py | 27 +++++++++---- mypy.ini | 14 +++++-- script/hassfest/mypy_config.py | 1 - 14 files changed, 186 insertions(+), 84 deletions(-) diff --git a/.strict-typing b/.strict-typing index 09578153163..7d2f83a0e8d 100644 --- a/.strict-typing +++ b/.strict-typing @@ -25,6 +25,7 @@ homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* +homeassistant.components.devolo_home_control.* homeassistant.components.dnsip.* homeassistant.components.dsmr.* homeassistant.components.dunehd.* diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 4c8757e4eff..297d3ac09c9 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -1,6 +1,10 @@ """The devolo_home_control integration.""" +from __future__ import annotations + import asyncio from functools import partial +from types import MappingProxyType +from typing import Any from devolo_home_control_api.exceptions.gateway import GatewayOfflineError from devolo_home_control_api.homecontrol import HomeControl @@ -9,7 +13,7 @@ from devolo_home_control_api.mydevolo import Mydevolo from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import ( @@ -37,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids) - if GATEWAY_SERIAL_PATTERN.match(entry.unique_id): + if entry.unique_id and GATEWAY_SERIAL_PATTERN.match(entry.unique_id): uuid = await hass.async_add_executor_job(mydevolo.uuid) hass.config_entries.async_update_entry(entry, unique_id=uuid) @@ -60,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - def shutdown(event): + def shutdown(event: Event) -> None: for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: gateway.websocket_disconnect( f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}" @@ -88,7 +92,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload -def configure_mydevolo(conf: dict) -> Mydevolo: +def configure_mydevolo(conf: dict[str, Any] | MappingProxyType[str, Any]) -> Mydevolo: """Configure mydevolo.""" mydevolo = Mydevolo() mydevolo.user = conf[CONF_USERNAME] diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index e99c96832ae..c8ce1c3585d 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -1,4 +1,9 @@ """Platform for binary sensor integration.""" +from __future__ import annotations + +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_HEAT, @@ -11,6 +16,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_device import DevoloDeviceEntity @@ -26,10 +32,10 @@ DEVICE_CLASS_MAPPING = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all binary sensor and multi level sensor devices and setup them via config entry.""" - entities = [] + entities: list[BinarySensorEntity] = [] for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: for device in gateway.binary_sensor_devices: @@ -61,7 +67,9 @@ async def async_setup_entry( class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): """Representation of a binary sensor within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: """Initialize a devolo binary sensor.""" self._binary_sensor_property = device_instance.binary_sensor_property.get( element_uid @@ -91,12 +99,12 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self._enabled_default = False @property - def is_on(self): + def is_on(self) -> bool: """Return the state.""" - return self._value + return bool(self._value) @property - def device_class(self): + def device_class(self) -> str | None: """Return device class.""" return self._device_class @@ -104,7 +112,13 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): """Representation of a remote control within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid, key): + def __init__( + self, + homecontrol: HomeControl, + device_instance: Zwave, + element_uid: str, + key: int, + ) -> None: """Initialize a devolo remote control.""" self._remote_control_property = device_instance.remote_control_property.get( element_uid @@ -120,11 +134,11 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): self._state = False @property - def is_on(self): + def is_on(self) -> bool: """Return the state.""" return self._state - def _sync(self, message): + def _sync(self, message: tuple) -> None: """Update the binary sensor state.""" if ( message[0] == self._remote_control_property.element_uid diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 018c9cf36ec..6b890544da5 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -1,6 +1,8 @@ """Platform for climate integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ( ATTR_TEMPERATURE, HVAC_MODE_HEAT, @@ -11,13 +13,14 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all cover devices and setup them via config entry.""" entities = [] @@ -82,12 +85,14 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit @property def min_temp(self) -> float: """Return the minimum set temperature value.""" - return self._multi_level_switch_property.min + min_temp: float = self._multi_level_switch_property.min + return min_temp @property def max_temp(self) -> float: """Return the maximum set temperature value.""" - return self._multi_level_switch_property.max + max_temp: float = self._multi_level_switch_property.max + return max_temp @property def precision(self) -> float: @@ -95,7 +100,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit return PRECISION_TENTHS @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_TARGET_TEMPERATURE @@ -107,6 +112,6 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit def set_hvac_mode(self, hvac_mode: str) -> None: """Do nothing as devolo devices do not support changing the hvac mode.""" - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" self._multi_level_switch_property.set(kwargs[ATTR_TEMPERATURE]) diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 10172b94452..e6b3dcbe329 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -1,9 +1,15 @@ """Config flow to configure the devolo home control integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import DiscoveryInfoType from . import configure_mydevolo @@ -16,16 +22,18 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize devolo Home Control flow.""" self.data_schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } - self._reauth_entry = None + self._reauth_entry: ConfigEntry | None = None self._url = DEFAULT_MYDEVOLO - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if self.show_advanced_options: self.data_schema[vol.Required(CONF_MYDEVOLO, default=self._url)] = str @@ -36,7 +44,9 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except CredentialsInvalid: return self._show_form(step_id="user", errors={"base": "invalid_auth"}) - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle zeroconf discovery.""" # Check if it is a gateway if discovery_info.get("properties", {}).get("MT") in SUPPORTED_MODEL_TYPES: @@ -44,7 +54,9 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_zeroconf_confirm() return self.async_abort(reason="Not a devolo Home Control gateway.") - async def async_step_zeroconf_confirm(self, user_input=None): + async def async_step_zeroconf_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by zeroconf.""" if user_input is None: return self._show_form(step_id="zeroconf_confirm") @@ -55,7 +67,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="zeroconf_confirm", errors={"base": "invalid_auth"} ) - async def async_step_reauth(self, user_input): + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: """Handle reauthentication.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -67,7 +79,9 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by reauthentication.""" if user_input is None: return self._show_form(step_id="reauth_confirm") @@ -82,7 +96,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", errors={"base": "reauth_failed"} ) - async def _connect_mydevolo(self, user_input): + async def _connect_mydevolo(self, user_input: dict[str, Any]) -> FlowResult: """Connect to mydevolo.""" user_input[CONF_MYDEVOLO] = user_input.get(CONF_MYDEVOLO, self._url) mydevolo = configure_mydevolo(conf=user_input) @@ -118,7 +132,9 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") @callback - def _show_form(self, step_id, errors=None): + def _show_form( + self, step_id: str, errors: dict[str, str] | None = None + ) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id=step_id, diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index d552c53bbfc..b2ea2f66b67 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -1,4 +1,8 @@ """Platform for cover integration.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.cover import ( DEVICE_CLASS_BLIND, SUPPORT_CLOSE, @@ -8,13 +12,14 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all cover devices and setup them via config entry.""" entities = [] @@ -38,33 +43,33 @@ class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity): """Representation of a cover device within devolo Home Control.""" @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position. 0 is closed. 100 is open.""" return self._value @property - def device_class(self): + def device_class(self) -> str: """Return the class of the device.""" return DEVICE_CLASS_BLIND @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the blind is closed or not.""" return not bool(self._value) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the blind.""" self._multi_level_switch_property.set(100) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the blind.""" self._multi_level_switch_property.set(0) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Set the blind to the given position.""" self._multi_level_switch_property.set(kwargs["position"]) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 6aef842ffff..e8eec3ca7dc 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -1,7 +1,12 @@ """Base class for a device entity integrated in devolo Home Control.""" +from __future__ import annotations + import logging -from homeassistant.helpers.entity import Entity +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .subscriber import Subscriber @@ -12,26 +17,30 @@ _LOGGER = logging.getLogger(__name__) class DevoloDeviceEntity(Entity): """Abstract representation of a device within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: """Initialize a devolo device entity.""" self._device_instance = device_instance self._unique_id = element_uid self._homecontrol = homecontrol - self._name = device_instance.settings_property["general_device_settings"].name + self._name: str = device_instance.settings_property[ + "general_device_settings" + ].name self._area = device_instance.settings_property["general_device_settings"].zone - self._device_class = None - self._value = None - self._unit = None + self._device_class: str | None = None + self._value: int + self._unit = "" self._enabled_default = True # This is not doing I/O. It fetches an internal state of the API - self._available = device_instance.is_online() + self._available: bool = device_instance.is_online() # Get the brand and model information self._brand = device_instance.brand self._model = device_instance.name - self.subscriber = None + self.subscriber: Subscriber | None = None self.sync_callback = self._sync async def async_added_to_hass(self) -> None: @@ -48,12 +57,12 @@ class DevoloDeviceEntity(Entity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the entity.""" return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self._device_instance.uid)}, @@ -69,12 +78,12 @@ class DevoloDeviceEntity(Entity): return self._enabled_default @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling state.""" return False @property - def name(self): + def name(self) -> str: """Return the display name of this entity.""" return self._name @@ -83,7 +92,7 @@ class DevoloDeviceEntity(Entity): """Return the online state.""" return self._available - def _sync(self, message): + def _sync(self, message: tuple) -> None: """Update the state.""" if message[0] == self._unique_id: self._value = message[1] @@ -91,7 +100,7 @@ class DevoloDeviceEntity(Entity): self._generic_message(message) self.schedule_update_ha_state() - def _generic_message(self, message): + def _generic_message(self, message: tuple) -> None: """Handle generic messages.""" if len(message) == 3 and message[2] == "battery_level": self._value = message[1] diff --git a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py index 482edd51f1e..eafd1e63b1f 100644 --- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py +++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py @@ -1,11 +1,16 @@ """Base class for multi level switches in devolo Home Control.""" +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from .devolo_device import DevoloDeviceEntity class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity): """Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: """Initialize a multi level switch within devolo Home Control.""" super().__init__( homecontrol=homecontrol, diff --git a/homeassistant/components/devolo_home_control/light.py b/homeassistant/components/devolo_home_control/light.py index 7fd59bd7d11..27c637cf114 100644 --- a/homeassistant/components/devolo_home_control/light.py +++ b/homeassistant/components/devolo_home_control/light.py @@ -1,4 +1,11 @@ """Platform for light integration.""" +from __future__ import annotations + +from typing import Any + +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, @@ -6,13 +13,14 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all light devices and setup them via config entry.""" entities = [] @@ -35,7 +43,9 @@ async def async_setup_entry( class DevoloLightDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, LightEntity): """Representation of a light within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: """Initialize a devolo multi level switch.""" super().__init__( homecontrol=homecontrol, @@ -48,21 +58,21 @@ class DevoloLightDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, LightEntity): ) @property - def brightness(self): + def brightness(self) -> int: """Return the brightness value of the light.""" return round(self._value / 100 * 255) @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the light.""" return bool(self._value) @property - def supported_features(self): + def supported_features(self) -> int: """Return the supported features.""" return SUPPORT_BRIGHTNESS - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if kwargs.get(ATTR_BRIGHTNESS) is not None: self._multi_level_switch_property.set( @@ -76,7 +86,7 @@ class DevoloLightDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, LightEntity): # If there is no binary switch attached to the device, turn it on to 100 %. self._multi_level_switch_property.set(100) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" if self._binary_switch_property is not None: self._binary_switch_property.set(False) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index e3091305375..3ab449b2c91 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -1,4 +1,9 @@ """Platform for sensor integration.""" +from __future__ import annotations + +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, @@ -12,6 +17,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_device import DevoloDeviceEntity @@ -28,10 +34,10 @@ DEVICE_CLASS_MAPPING = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all sensor devices and setup them via config entry.""" - entities = [] + entities: list[SensorEntity] = [] for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: for device in gateway.multi_level_sensor_devices: @@ -71,17 +77,17 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity): """Abstract representation of a multi level sensor within devolo Home Control.""" @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return device class.""" return self._device_class @property - def state(self): + def state(self) -> int: """Return the state of the sensor.""" return self._value @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return self._unit @@ -91,10 +97,10 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): def __init__( self, - homecontrol, - device_instance, - element_uid, - ): + homecontrol: HomeControl, + device_instance: Zwave, + element_uid: str, + ) -> None: """Initialize a devolo multi level sensor.""" self._multi_level_sensor_property = device_instance.multi_level_sensor_property[ element_uid @@ -123,7 +129,9 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): """Representation of a battery entity within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: """Initialize a battery sensor.""" super().__init__( @@ -141,7 +149,13 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): """Representation of a consumption entity within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid, consumption): + def __init__( + self, + homecontrol: HomeControl, + device_instance: Zwave, + element_uid: str, + consumption: str, + ) -> None: """Initialize a devolo consumption sensor.""" super().__init__( @@ -163,11 +177,11 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): self._name += f" {consumption}" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the entity.""" return f"{self._unique_id}_{self._sensor_type}" - def _sync(self, message): + def _sync(self, message: tuple) -> None: """Update the consumption sensor state.""" if message[0] == self._unique_id: self._value = getattr( diff --git a/homeassistant/components/devolo_home_control/subscriber.py b/homeassistant/components/devolo_home_control/subscriber.py index d291e4b174f..9899aa3a587 100644 --- a/homeassistant/components/devolo_home_control/subscriber.py +++ b/homeassistant/components/devolo_home_control/subscriber.py @@ -1,6 +1,7 @@ """Subscriber for devolo home control API publisher.""" import logging +from typing import Callable _LOGGER = logging.getLogger(__name__) @@ -8,12 +9,12 @@ _LOGGER = logging.getLogger(__name__) class Subscriber: """Subscriber class for the publisher in mprm websocket class.""" - def __init__(self, name, callback): + def __init__(self, name: str, callback: Callable) -> None: """Initiate the subscriber.""" self.name = name self.callback = callback - def update(self, message): + def update(self, message: str) -> None: """Trigger hass to update the device.""" _LOGGER.debug('%s got message "%s"', self.name, message) self.callback(message) diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index 2a96198826b..dcfa22db692 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -1,14 +1,22 @@ """Platform for switch integration.""" +from __future__ import annotations + +from typing import Any + +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_device import DevoloDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all devices and setup the switch devices via config entry.""" entities = [] @@ -33,7 +41,9 @@ async def async_setup_entry( class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): """Representation of a switch.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: """Initialize an devolo Switch.""" super().__init__( homecontrol=homecontrol, @@ -43,7 +53,8 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): self._binary_switch_property = self._device_instance.binary_switch_property.get( self._unique_id ) - self._is_on = self._binary_switch_property.state + self._is_on: bool = self._binary_switch_property.state + self._consumption: float | None if hasattr(self._device_instance, "consumption_property"): self._consumption = self._device_instance.consumption_property.get( @@ -53,26 +64,26 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): self._consumption = None @property - def is_on(self): + def is_on(self) -> bool: """Return the state.""" return self._is_on @property - def current_power_w(self): + def current_power_w(self) -> float | None: """Return the current consumption.""" return self._consumption - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Switch on the device.""" self._is_on = True self._binary_switch_property.set(state=True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Switch off the device.""" self._is_on = False self._binary_switch_property.set(state=False) - def _sync(self, message): + def _sync(self, message: tuple) -> None: """Update the binary switch state and consumption.""" if message[0].startswith("devolo.BinarySwitch"): self._is_on = self._device_instance.binary_switch_property[message[0]].state diff --git a/mypy.ini b/mypy.ini index 5f2eeb11c7d..09869a5b5fe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -286,6 +286,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.devolo_home_control.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.dnsip.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1063,9 +1074,6 @@ ignore_errors = true [mypy-homeassistant.components.denonavr.*] ignore_errors = true -[mypy-homeassistant.components.devolo_home_control.*] -ignore_errors = true - [mypy-homeassistant.components.dhcp.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e59a59de7c9..c63033dacc8 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -42,7 +42,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.deconz.*", "homeassistant.components.demo.*", "homeassistant.components.denonavr.*", - "homeassistant.components.devolo_home_control.*", "homeassistant.components.dhcp.*", "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", From 8347cf3731ceadfdb9dbd1ab8a8df1c41df03026 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 2 Jul 2021 19:21:05 +0200 Subject: [PATCH 048/818] Fix Fritz call deflection list (#52443) --- homeassistant/components/fritz/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 09bae36a29c..d9690b64069 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -110,7 +110,10 @@ def get_deflections( if not deflection_list: return [] - return [xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"]] + items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] + if not isinstance(items, list): + return [items] + return items def deflection_entities_list( From 7f3f6757eaa95eb5356118e4fbbcc742c04491fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 20:55:40 +0200 Subject: [PATCH 049/818] Fix Statistics recorder migration order (#52449) --- homeassistant/components/recorder/migration.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 30a5162e947..e931abbd2d3 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -347,7 +347,7 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): ) -def _apply_update(engine, session, new_version, old_version): +def _apply_update(engine, session, new_version, old_version): # noqa: C901 """Perform operations to bring schema up to date.""" connection = session.connection() if new_version == 1: @@ -463,12 +463,15 @@ def _apply_update(engine, session, new_version, old_version): # This dropped the statistics table, done again in version 18. pass elif new_version == 18: - if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - # Recreate the statistics and statisticsmeta tables - Statistics.__table__.drop(engine) - Statistics.__table__.create(engine) + # Recreate the statisticsmeta tables + if sqlalchemy.inspect(engine).has_table(StatisticsMeta.__tablename__): StatisticsMeta.__table__.drop(engine) - StatisticsMeta.__table__.create(engine) + StatisticsMeta.__table__.create(engine) + + # Recreate the statistics table + if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + Statistics.__table__.drop(engine) + Statistics.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") From b91b553ce52b76c7b2b129c055e27842e72c9911 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 20:56:51 +0200 Subject: [PATCH 050/818] Abort existing reauth flow on entry removal (#52407) --- homeassistant/config_entries.py | 13 +++++++++++++ tests/test_config_entries.py | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 49892937217..2bb8c4f3e29 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -819,6 +819,19 @@ class ConfigEntries: dev_reg.async_clear_config_entry(entry_id) ent_reg.async_clear_config_entry(entry_id) + # If the configuration entry is removed during reauth, it should + # abort any reauth flow that is active for the removed entry. + for progress_flow in self.hass.config_entries.flow.async_progress(): + context = progress_flow.get("context") + if ( + context + and context["source"] == SOURCE_REAUTH + and "entry_id" in context + and context["entry_id"] == entry_id + and "flow_id" in progress_flow + ): + self.hass.config_entries.flow.async_abort(progress_flow["flow_id"]) + # After we have fully removed an "ignore" config entry we can try and rediscover it so that a # user is able to immediately start configuring it. We do this by starting a new flow with # the 'unignore' step. If the integration doesn't implement async_step_unignore then diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 615b97fb990..7bcc83048a4 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -337,6 +337,30 @@ async def test_remove_entry(hass, manager): assert not entity_entry_list +async def test_remove_entry_cancels_reauth(hass, manager): + """Tests that removing a config entry, also aborts existing reauth flows.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(side_effect=ConfigEntryAuthFailed()) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + entry.add_to_hass(hass) + await entry.async_setup(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + + await manager.async_remove(entry.entry_id) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + async def test_remove_entry_handles_callback_error(hass, manager): """Test that exceptions in the remove callback are handled.""" mock_setup_entry = AsyncMock(return_value=True) From 40b629cd066f265bfba93dd860893bf564a4307a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 2 Jul 2021 16:23:17 -0500 Subject: [PATCH 051/818] Replace custom listener with helper in ReCollect Waste (#52445) --- homeassistant/components/recollect_waste/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 1ef05dbeae9..ce4577321d3 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -13,8 +13,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER -DATA_LISTENER = "listener" - DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) @@ -23,7 +21,7 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) session = aiohttp_client.async_get_clientsession(hass) client = Client( @@ -55,9 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( - async_reload_entry - ) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True @@ -72,7 +68,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) - cancel_listener() return unload_ok From 4b7442412abf6d3ded04adabca4af4a12cf5041c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 3 Jul 2021 00:09:34 +0000 Subject: [PATCH 052/818] [ci skip] Translation update --- .../alarm_control_panel/translations/ca.json | 4 ++++ .../alarm_control_panel/translations/de.json | 4 ++++ .../alarm_control_panel/translations/nl.json | 4 ++++ .../demo/translations/select.nl.json | 9 +++++++ .../forecast_solar/translations/en.json | 2 +- .../forecast_solar/translations/et.json | 2 +- .../forecast_solar/translations/nl.json | 23 ++++++++++++++++++ .../freedompro/translations/nl.json | 20 ++++++++++++++++ .../components/motioneye/translations/nl.json | 10 ++++++++ .../nmap_tracker/translations/de.json | 4 +++- .../nmap_tracker/translations/nl.json | 24 +++++++++++++++++-- 11 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/demo/translations/select.nl.json create mode 100644 homeassistant/components/forecast_solar/translations/nl.json create mode 100644 homeassistant/components/freedompro/translations/nl.json diff --git a/homeassistant/components/alarm_control_panel/translations/ca.json b/homeassistant/components/alarm_control_panel/translations/ca.json index dafef96b090..d576b5b629a 100644 --- a/homeassistant/components/alarm_control_panel/translations/ca.json +++ b/homeassistant/components/alarm_control_panel/translations/ca.json @@ -4,6 +4,7 @@ "arm_away": "Activa {entity_name} fora", "arm_home": "Activa {entity_name} a casa", "arm_night": "Activa {entity_name} nocturn", + "arm_vacation": "Activa {entity_name} en mode vacances", "disarm": "Desactiva {entity_name}", "trigger": "Dispara {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} est\u00e0 activada en mode 'a fora'", "is_armed_home": "{entity_name} est\u00e0 activada en mode 'a casa'", "is_armed_night": "{entity_name} est\u00e0 activada en mode 'nocturn'", + "is_armed_vacation": "{entity_name} activada en mode vacances", "is_disarmed": "{entity_name} est\u00e0 desactivada", "is_triggered": "{entity_name} est\u00e0 disparada" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} activada en mode 'a fora'", "armed_home": "{entity_name} activada en mode 'a casa'", "armed_night": "{entity_name} activada en mode 'nocturn'", + "armed_vacation": "{entity_name} s'activa en mode vacances", "disarmed": "{entity_name} desactivada", "triggered": "{entity_name} disparat/ada" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Activada, bypass personalitzat", "armed_home": "Activada, mode a casa", "armed_night": "Activada, mode nocturn", + "armed_vacation": "Activada, mode vacances", "arming": "Activant", "disarmed": "Desactivada", "disarming": "Desactivant", diff --git a/homeassistant/components/alarm_control_panel/translations/de.json b/homeassistant/components/alarm_control_panel/translations/de.json index a671c388932..379ddfc041d 100644 --- a/homeassistant/components/alarm_control_panel/translations/de.json +++ b/homeassistant/components/alarm_control_panel/translations/de.json @@ -4,6 +4,7 @@ "arm_away": "Aktiviere {entity_name} Unterwegs", "arm_home": "Aktiviere {entity_name} Zuhause", "arm_night": "Aktiviere {entity_name} Nacht-Modus", + "arm_vacation": "Aktiviere {entity_name} Urlaub", "disarm": "Deaktivere {entity_name}", "trigger": "Ausl\u00f6ser {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} ist aktiviert - Unterwegs", "is_armed_home": "{entity_name} ist aktiviert - Zuhause", "is_armed_night": "{entity_name} ist aktiviert - Nacht", + "is_armed_vacation": "{entity_name} ist aktiviert - Urlaub", "is_disarmed": "{entity_name} ist deaktiviert", "is_triggered": "{entity_name} wurde ausgel\u00f6st" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} Unterwegs", "armed_home": "{entity_name} Zuhause", "armed_night": "{entity_name} Nacht-Modus", + "armed_vacation": "{entity_name} Urlaub", "disarmed": "{entity_name} deaktiviert", "triggered": "{entity_name} ausgel\u00f6st" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Aktiv, benutzerdefiniert", "armed_home": "Aktiv, zu Hause", "armed_night": "Aktiv, Nacht", + "armed_vacation": "Aktiv, Urlaub", "arming": "Aktiviere", "disarmed": "Inaktiv", "disarming": "Deaktiviere", diff --git a/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant/components/alarm_control_panel/translations/nl.json index 0a0f33d6181..65b7cf1a4b8 100644 --- a/homeassistant/components/alarm_control_panel/translations/nl.json +++ b/homeassistant/components/alarm_control_panel/translations/nl.json @@ -4,6 +4,7 @@ "arm_away": "Inschakelen {entity_name} afwezig", "arm_home": "Inschakelen {entity_name} thuis", "arm_night": "Inschakelen {entity_name} nacht", + "arm_vacation": "Schakel {entity_name} in op vakantie", "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} afwezig ingeschakeld", "is_armed_home": "{entity_name} thuis ingeschakeld", "is_armed_night": "{entity_name} nachtstand ingeschakeld", + "is_armed_vacation": "{entity_name} is in vakantie geschakeld", "is_disarmed": "{entity_name} is uitgeschakeld", "is_triggered": "{entity_name} wordt geactiveerd" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} afwezig ingeschakeld", "armed_home": "{entity_name} thuis ingeschakeld", "armed_night": "{entity_name} nachtstand ingeschakeld", + "armed_vacation": "{entity_name} schakelde vakantie in", "disarmed": "{entity_name} uitgeschakeld", "triggered": "{entity_name} geactiveerd" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", "armed_home": "Ingeschakeld thuis", "armed_night": "Ingeschakeld nacht", + "armed_vacation": "Vakantie ingeschakeld", "arming": "Schakelt in", "disarmed": "Uitgeschakeld", "disarming": "Schakelt uit", diff --git a/homeassistant/components/demo/translations/select.nl.json b/homeassistant/components/demo/translations/select.nl.json new file mode 100644 index 00000000000..4312d8c4d34 --- /dev/null +++ b/homeassistant/components/demo/translations/select.nl.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Lichtsnelheid", + "ludicrous_speed": "Lachwekkende snelheid", + "ridiculous_speed": "Belachelijke snelheid" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index 6de9cddc567..f9eef2b5c0a 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -24,7 +24,7 @@ "declination": "Declination (0 = Horizontal, 90 = Vertical)", "modules power": "Total Watt peak power of your solar modules" }, - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation is a field is unclear." + "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear." } } } diff --git a/homeassistant/components/forecast_solar/translations/et.json b/homeassistant/components/forecast_solar/translations/et.json index 6dddf4a7496..7aa87f4cf58 100644 --- a/homeassistant/components/forecast_solar/translations/et.json +++ b/homeassistant/components/forecast_solar/translations/et.json @@ -24,7 +24,7 @@ "declination": "Deklinatsioon (0 = horisontaalne, 90 = vertikaalne)", "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides" }, - "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui on ebaselge." + "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui asi on ebaselge." } } } diff --git a/homeassistant/components/forecast_solar/translations/nl.json b/homeassistant/components/forecast_solar/translations/nl.json new file mode 100644 index 00000000000..c21f631af7e --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API-sleutel (optioneel)" + }, + "description": "Met deze waarden kan het resultaat van Solar.Forecast worden aangepast. Raadpleeg de documentatie als een veld onduidelijk is." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/nl.json b/homeassistant/components/freedompro/translations/nl.json new file mode 100644 index 00000000000..8bf5e60d937 --- /dev/null +++ b/homeassistant/components/freedompro/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Voer de API-sleutel in die is verkregen van https://home.freedompro.eu", + "title": "Freedompro API key" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json index 0fd3c7661eb..81bb0365c33 100644 --- a/homeassistant/components/motioneye/translations/nl.json +++ b/homeassistant/components/motioneye/translations/nl.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "MotionEye-webhooks configureren om gebeurtenissen aan Home Assistant te melden", + "webhook_set_overwrite": "Overschrijf niet-herkende webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json index 351c09036fe..1893eb28b08 100644 --- a/homeassistant/components/nmap_tracker/translations/de.json +++ b/homeassistant/components/nmap_tracker/translations/de.json @@ -28,7 +28,9 @@ "exclude": "Netzwerkadressen (kommagetrennt), die von der \u00dcberpr\u00fcfung ausgeschlossen werden sollen", "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", "hosts": "Zu scannende Netzwerkadressen (kommagetrennt)", - "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap" + "interval_seconds": "Scanintervall", + "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap", + "track_new_devices": "Neue Ger\u00e4te verfolgen" }, "description": "Konfiguriere die Hosts, die von Nmap gescannt werden sollen. Netzwerkadresse und Ausschl\u00fcsse k\u00f6nnen IP-Adressen (192.168.1.1), IP-Netzwerke (192.168.0.0/24) oder IP-Bereiche (192.168.1.0-32) sein." } diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json index 9f52a6b9add..b1812db53b3 100644 --- a/homeassistant/components/nmap_tracker/translations/nl.json +++ b/homeassistant/components/nmap_tracker/translations/nl.json @@ -9,8 +9,28 @@ "step": { "user": { "data": { - "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen" - } + "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen", + "home_interval": "Minimum aantal minuten tussen scans van actieve apparaten (batterij sparen)", + "hosts": "Netwerkadressen (gescheiden door komma's) om te scannen", + "scan_options": "Ruwe configureerbare scanopties voor Nmap" + }, + "description": "Configureer hosts die moeten worden gescand door Nmap. Netwerkadres en uitsluitingen kunnen IP-adressen (192.168.1.1), IP-netwerken (192.168.0.0/24) of IP-bereiken (192.168.1.0-32) zijn." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Ongeldige hosts" + }, + "step": { + "init": { + "data": { + "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen", + "home_interval": "Minimum aantal minuten tussen scans van actieve apparaten (batterij sparen)", + "hosts": "Netwerkadressen (gescheiden door komma's) om te scannen", + "scan_options": "Ruwe configureerbare scanopties voor Nmap" + }, + "description": "Configureer hosts die moeten worden gescand door Nmap. Netwerkadres en uitsluitingen kunnen IP-adressen (192.168.1.1), IP-netwerken (192.168.0.0/24) of IP-bereiken (192.168.1.0-32) zijn." } } }, From 69b9a9c4ee9096274c72066dc55e3204b445866b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 3 Jul 2021 15:26:43 +0200 Subject: [PATCH 053/818] Merge fritzbox_netmonitor integration into fritz (#52264) * Merge netmonitor integration * cleanup additional files * Use attrs instead of properties * Add state_class to relevant sensors * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * mypy fix + small cleanup * Round, GB and icons * Update homeassistant/components/fritz/sensor.py Co-authored-by: Franck Nijhof * remove state_class from no-reset counters Co-authored-by: Franck Nijhof --- .coveragerc | 1 - homeassistant/components/fritz/sensor.py | 125 +++++++++++------ .../fritzbox_netmonitor/__init__.py | 1 - .../fritzbox_netmonitor/manifest.json | 8 -- .../components/fritzbox_netmonitor/sensor.py | 131 ------------------ requirements_all.txt | 1 - requirements_test_all.txt | 1 - 7 files changed, 84 insertions(+), 184 deletions(-) delete mode 100644 homeassistant/components/fritzbox_netmonitor/__init__.py delete mode 100644 homeassistant/components/fritzbox_netmonitor/manifest.json delete mode 100644 homeassistant/components/fritzbox_netmonitor/sensor.py diff --git a/.coveragerc b/.coveragerc index cbdcc60b421..6bf9936e26b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -348,7 +348,6 @@ omit = homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/sensor.py - homeassistant/components/fritzbox_netmonitor/sensor.py homeassistant/components/fronius/sensor.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 6d3a8f33c3c..042ec069864 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -8,7 +8,7 @@ from typing import Callable, TypedDict from fritzconnection.core.exceptions import FritzConnectionException from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant @@ -42,11 +42,43 @@ def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: return status.external_ip -class SensorData(TypedDict): +def _retrieve_kib_s_sent_state(status: FritzStatus, last_value: str) -> str: + """Return upload transmission rate.""" + return round(status.transmission_rate[0] * 8 / 1024, 1) + + +def _retrieve_kib_s_received_state(status: FritzStatus, last_value: str) -> str: + """Return download transmission rate.""" + return round(status.transmission_rate[1] * 8 / 1024, 1) + + +def _retrieve_max_kib_s_sent_state(status: FritzStatus, last_value: str) -> str: + """Return upload max transmission rate.""" + return round(status.max_bit_rate[0] / 1024, 1) + + +def _retrieve_max_kib_s_received_state(status: FritzStatus, last_value: str) -> str: + """Return download max transmission rate.""" + return round(status.max_bit_rate[1] / 1024, 1) + + +def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> str: + """Return upload total data.""" + return round(status.bytes_sent * 8 / 1024 / 1024 / 1024, 1) + + +def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> str: + """Return download total data.""" + return round(status.bytes_received * 8 / 1024 / 1024 / 1024, 1) + + +class SensorData(TypedDict, total=False): """Sensor data class.""" name: str device_class: str | None + state_class: str | None + unit_of_measurement: str | None icon: str | None state_provider: Callable @@ -54,16 +86,52 @@ class SensorData(TypedDict): SENSOR_DATA = { "external_ip": SensorData( name="External IP", - device_class=None, icon="mdi:earth", state_provider=_retrieve_external_ip_state, ), "uptime": SensorData( name="Uptime", device_class=DEVICE_CLASS_TIMESTAMP, - icon=None, state_provider=_retrieve_uptime_state, ), + "kib_s_sent": SensorData( + name="KiB/s sent", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement="KiB/s", + icon="mdi:upload", + state_provider=_retrieve_kib_s_sent_state, + ), + "kib_s_received": SensorData( + name="KiB/s received", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement="KiB/s", + icon="mdi:download", + state_provider=_retrieve_kib_s_received_state, + ), + "max_kib_s_sent": SensorData( + name="Max KiB/s sent", + unit_of_measurement="KiB/s", + icon="mdi:upload", + state_provider=_retrieve_max_kib_s_sent_state, + ), + "max_kib_s_received": SensorData( + name="Max KiB/s received", + unit_of_measurement="KiB/s", + icon="mdi:download", + state_provider=_retrieve_max_kib_s_received_state, + ), + "mb_sent": SensorData( + name="GB sent", + unit_of_measurement="GB", + icon="mdi:upload", + state_provider=_retrieve_gb_sent_state, + ), + "mb_received": SensorData( + name="GB received", + unit_of_measurement="GB", + icon="mdi:download", + state_provider=_retrieve_gb_received_state, + ), } @@ -97,11 +165,14 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): ) -> None: """Init FRITZ!Box connectivity class.""" self._sensor_data: SensorData = SENSOR_DATA[sensor_type] - self._unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" - self._name = f"{device_friendly_name} {self._sensor_data['name']}" - self._is_available = True self._last_value: str | None = None - self._state: str | None = None + self._attr_available = True + self._attr_device_class = self._sensor_data.get("device_class") + self._attr_icon = self._sensor_data.get("icon") + self._attr_name = f"{device_friendly_name} {self._sensor_data['name']}" + self._attr_state_class = self._sensor_data.get("state_class") + self._attr_unit_of_measurement = self._sensor_data.get("unit_of_measurement") + self._attr_unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" super().__init__(fritzbox_tools, device_friendly_name) @property @@ -109,46 +180,18 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): """Return the state provider for the binary sensor.""" return self._sensor_data["state_provider"] - @property - def name(self) -> str: - """Return name.""" - return self._name - - @property - def device_class(self) -> str | None: - """Return device class.""" - return self._sensor_data["device_class"] - - @property - def icon(self) -> str | None: - """Return icon.""" - return self._sensor_data["icon"] - - @property - def unique_id(self) -> str: - """Return unique id.""" - return self._unique_id - - @property - def state(self) -> str | None: - """Return the state of the sensor.""" - return self._state - - @property - def available(self) -> bool: - """Return availability.""" - return self._is_available - def update(self) -> None: """Update data.""" _LOGGER.debug("Updating FRITZ!Box sensors") try: status: FritzStatus = self._fritzbox_tools.fritz_status - self._is_available = True + self._attr_available = True except FritzConnectionException: _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) - self._is_available = False + self._attr_available = False return - self._state = self._last_value = self._state_provider(status, self._last_value) + self._attr_state = self._last_value = self._state_provider( + status, self._last_value + ) diff --git a/homeassistant/components/fritzbox_netmonitor/__init__.py b/homeassistant/components/fritzbox_netmonitor/__init__.py deleted file mode 100644 index 8bea1da4a44..00000000000 --- a/homeassistant/components/fritzbox_netmonitor/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The fritzbox_netmonitor component.""" diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json deleted file mode 100644 index b52872fc044..00000000000 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "fritzbox_netmonitor", - "name": "AVM FRITZ!Box Net Monitor", - "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==1.4.2"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py deleted file mode 100644 index 3c37de7664c..00000000000 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Support for monitoring an AVM Fritz!Box router.""" -from datetime import timedelta -import logging - -from fritzconnection.core.exceptions import FritzConnectionException -from fritzconnection.lib.fritzstatus import FritzStatus -from requests.exceptions import RequestException -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "fritz_netmonitor" -DEFAULT_HOST = "169.254.1.1" # This IP is valid for all FRITZ!Box routers. - -ATTR_BYTES_RECEIVED = "bytes_received" -ATTR_BYTES_SENT = "bytes_sent" -ATTR_TRANSMISSION_RATE_UP = "transmission_rate_up" -ATTR_TRANSMISSION_RATE_DOWN = "transmission_rate_down" -ATTR_EXTERNAL_IP = "external_ip" -ATTR_IS_CONNECTED = "is_connected" -ATTR_IS_LINKED = "is_linked" -ATTR_MAX_BYTE_RATE_DOWN = "max_byte_rate_down" -ATTR_MAX_BYTE_RATE_UP = "max_byte_rate_up" -ATTR_UPTIME = "uptime" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) - -STATE_ONLINE = "online" -STATE_OFFLINE = "offline" - -ICON = "mdi:web" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the FRITZ!Box monitor sensors.""" - name = config[CONF_NAME] - host = config[CONF_HOST] - - try: - fstatus = FritzStatus(address=host) - except (ValueError, TypeError, FritzConnectionException): - fstatus = None - - if fstatus is None: - _LOGGER.error("Failed to establish connection to FRITZ!Box: %s", host) - return 1 - _LOGGER.info("Successfully connected to FRITZ!Box") - - add_entities([FritzboxMonitorSensor(name, fstatus)], True) - - -class FritzboxMonitorSensor(SensorEntity): - """Implementation of a fritzbox monitor sensor.""" - - def __init__(self, name, fstatus): - """Initialize the sensor.""" - self._name = name - self._fstatus = fstatus - self._state = STATE_UNAVAILABLE - self._is_linked = self._is_connected = None - self._external_ip = self._uptime = None - self._bytes_sent = self._bytes_received = None - self._transmission_rate_up = None - self._transmission_rate_down = None - self._max_byte_rate_up = self._max_byte_rate_down = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name.rstrip() - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - # Don't return attributes if FritzBox is unreachable - if self._state == STATE_UNAVAILABLE: - return {} - return { - ATTR_IS_LINKED: self._is_linked, - ATTR_IS_CONNECTED: self._is_connected, - ATTR_EXTERNAL_IP: self._external_ip, - ATTR_UPTIME: self._uptime, - ATTR_BYTES_SENT: self._bytes_sent, - ATTR_BYTES_RECEIVED: self._bytes_received, - ATTR_TRANSMISSION_RATE_UP: self._transmission_rate_up, - ATTR_TRANSMISSION_RATE_DOWN: self._transmission_rate_down, - ATTR_MAX_BYTE_RATE_UP: self._max_byte_rate_up, - ATTR_MAX_BYTE_RATE_DOWN: self._max_byte_rate_down, - } - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Retrieve information from the FritzBox.""" - try: - self._is_linked = self._fstatus.is_linked - self._is_connected = self._fstatus.is_connected - self._external_ip = self._fstatus.external_ip - self._uptime = self._fstatus.uptime - self._bytes_sent = self._fstatus.bytes_sent - self._bytes_received = self._fstatus.bytes_received - transmission_rate = self._fstatus.transmission_rate - self._transmission_rate_up = transmission_rate[0] - self._transmission_rate_down = transmission_rate[1] - self._max_byte_rate_up = self._fstatus.max_byte_rate[0] - self._max_byte_rate_down = self._fstatus.max_byte_rate[1] - self._state = STATE_ONLINE if self._is_connected else STATE_OFFLINE - except RequestException as err: - self._state = STATE_UNAVAILABLE - _LOGGER.warning("Could not reach FRITZ!Box: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 714b7495a18..c4be3f3a711 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,6 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -# homeassistant.components.fritzbox_netmonitor fritzconnection==1.4.2 # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51483cff9f3..b06ef05ef64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,6 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -# homeassistant.components.fritzbox_netmonitor fritzconnection==1.4.2 # homeassistant.components.fritz From 0cb61b628d1a82e5fd26790fcf625866727c59f3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 3 Jul 2021 15:37:54 +0200 Subject: [PATCH 054/818] Improve typing in Sony Bravia TV integration (#52438) * Strict typing * Variables typing * Suggested change * Fix pylint * Use suppress instead of try..except * Remove unused variables * Suggested change * Fix pylint * Fix typing for unique_id --- .strict-typing | 1 + homeassistant/components/braviatv/__init__.py | 100 ++++++++++-------- .../components/braviatv/config_flow.py | 67 +++++++----- homeassistant/components/braviatv/const.py | 24 +++-- .../components/braviatv/media_player.py | 69 +++++++----- homeassistant/components/braviatv/remote.py | 36 +++++-- mypy.ini | 11 ++ 7 files changed, 195 insertions(+), 113 deletions(-) diff --git a/.strict-typing b/.strict-typing index 7d2f83a0e8d..dbf25ac927e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -18,6 +18,7 @@ homeassistant.components.automation.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bond.* +homeassistant.components.braviatv.* homeassistant.components.brother.* homeassistant.components.calendar.* homeassistant.components.camera.* diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index eecf8533800..744c856a143 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,14 +1,20 @@ """The Bravia TV component.""" +from __future__ import annotations + import asyncio +from collections.abc import Iterable from datetime import timedelta import logging +from typing import Final from bravia_tv import BraviaRC from bravia_tv.braviarc import NoIPControl from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -16,11 +22,11 @@ from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME _LOGGER = logging.getLogger(__name__) -PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] -SCAN_INTERVAL = timedelta(seconds=10) +PLATFORMS: Final[list[str]] = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +SCAN_INTERVAL: Final = timedelta(seconds=10) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up a config entry.""" host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] @@ -40,7 +46,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS @@ -52,7 +58,7 @@ async def async_unload_entry(hass, config_entry): return unload_ok -async def update_listener(hass, config_entry): +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) @@ -64,28 +70,33 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): several platforms. """ - def __init__(self, hass, host, mac, pin, ignored_sources): + def __init__( + self, + hass: HomeAssistant, + host: str, + mac: str, + pin: str, + ignored_sources: list[str], + ) -> None: """Initialize Bravia TV Client.""" self.braviarc = BraviaRC(host, mac) self.pin = pin self.ignored_sources = ignored_sources - self.muted = False - self.channel_name = None - self.channel_number = None - self.media_title = None - self.source = None - self.source_list = [] - self.original_content_list = [] - self.content_mapping = {} - self.duration = None - self.content_uri = None - self.start_date_time = None - self.program_media_type = None - self.audio_output = None - self.min_volume = None - self.max_volume = None - self.volume_level = None + self.muted: bool = False + self.channel_name: str | None = None + self.media_title: str | None = None + self.source: str | None = None + self.source_list: list[str] = [] + self.original_content_list: list[str] = [] + self.content_mapping: dict[str, str] = {} + self.duration: int | None = None + self.content_uri: str | None = None + self.program_media_type: str | None = None + self.audio_output: str | None = None + self.min_volume: int | None = None + self.max_volume: int | None = None + self.volume_level: float | None = None self.is_on = False # Assume that the TV is in Play mode self.playing = True @@ -101,19 +112,20 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): ), ) - def _send_command(self, command, repeats=1): + def _send_command(self, command: str, repeats: int = 1) -> None: """Send a command to the TV.""" for _ in range(repeats): for cmd in command: self.braviarc.send_command(cmd) - def _get_source(self): + def _get_source(self) -> str | None: """Return the name of the source.""" for key, value in self.content_mapping.items(): if value == self.content_uri: return key + return None - def _refresh_volume(self): + def _refresh_volume(self) -> bool: """Refresh volume information.""" volume_info = self.braviarc.get_volume_info(self.audio_output) if volume_info is not None: @@ -122,11 +134,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.audio_output = volume_info.get("target") self.min_volume = volume_info.get("minVolume") self.max_volume = volume_info.get("maxVolume") - self.muted = volume_info.get("mute") + self.muted = volume_info.get("mute", False) return True return False - def _refresh_channels(self): + def _refresh_channels(self) -> bool: """Refresh source and channels list.""" if not self.source_list: self.content_mapping = self.braviarc.load_source_list() @@ -138,17 +150,15 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.source_list.append(key) return True - def _refresh_playing_info(self): + def _refresh_playing_info(self) -> None: """Refresh playing information.""" playing_info = self.braviarc.get_playing_info() program_name = playing_info.get("programTitle") self.channel_name = playing_info.get("title") self.program_media_type = playing_info.get("programMediaType") - self.channel_number = playing_info.get("dispNum") self.content_uri = playing_info.get("uri") self.source = self._get_source() self.duration = playing_info.get("durationSec") - self.start_date_time = playing_info.get("startDateTime") if not playing_info: self.channel_name = "App" if self.channel_name is not None: @@ -158,7 +168,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): else: self.media_title = None - def _update_tv_data(self): + def _update_tv_data(self) -> None: """Connect and update TV info.""" power_status = self.braviarc.get_power_status() @@ -182,26 +192,26 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.is_on = False - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch the latest data.""" if self.state_lock.locked(): return await self.hass.async_add_executor_job(self._update_tv_data) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the device on.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.turn_on) await self.async_request_refresh() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off device.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.turn_off) await self.async_request_refresh() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" async with self.state_lock: await self.hass.async_add_executor_job( @@ -209,7 +219,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): ) await self.async_request_refresh() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command to device.""" async with self.state_lock: await self.hass.async_add_executor_job( @@ -217,7 +227,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): ) await self.async_request_refresh() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command to device.""" async with self.state_lock: await self.hass.async_add_executor_job( @@ -225,46 +235,46 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): ) await self.async_request_refresh() - async def async_volume_mute(self, mute): + async def async_volume_mute(self, mute: bool) -> None: """Send mute command to device.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.mute_volume, mute) await self.async_request_refresh() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command to device.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.media_play) self.playing = True await self.async_request_refresh() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command to device.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.media_pause) self.playing = False await self.async_request_refresh() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command to device.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.media_stop) self.playing = False await self.async_request_refresh() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.media_next_track) await self.async_request_refresh() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" async with self.state_lock: await self.hass.async_add_executor_job(self.braviarc.media_previous_track) await self.async_request_refresh() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set the input source.""" if source in self.content_mapping: uri = self.content_mapping[source] @@ -272,7 +282,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.hass.async_add_executor_job(self.braviarc.play_content, uri) await self.async_request_refresh() - async def async_send_command(self, command, repeats): + async def async_send_command(self, command: Iterable[str], repeats: int) -> None: """Send command to device.""" async with self.state_lock: await self.hass.async_add_executor_job(self._send_command, command, repeats) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 0813a3e52c5..159f3806d61 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,14 +1,20 @@ """Adds config flow for Bravia TV integration.""" +from __future__ import annotations + +from contextlib import suppress import ipaddress import re +from typing import Any from bravia_tv import BraviaRC from bravia_tv.braviarc import NoIPControl import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -22,14 +28,13 @@ from .const import ( ) -def host_valid(host): +def host_valid(host: str) -> bool: """Return True if hostname or IP address is valid.""" - try: + with suppress(ValueError): if ipaddress.ip_address(host).version in [4, 6]: return True - except ValueError: - disallowed = re.compile(r"[^a-zA-Z\d\-]") - return all(x and not disallowed.search(x) for x in host.split(".")) + disallowed = re.compile(r"[^a-zA-Z\d\-]") + return all(x and not disallowed.search(x) for x in host.split(".")) class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -37,15 +42,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize.""" - self.braviarc = None - self.host = None - self.title = None - self.mac = None + self.braviarc: BraviaRC | None = None + self.host: str | None = None + self.title = "" + self.mac: str | None = None - async def init_device(self, pin): + async def init_device(self, pin: str) -> None: """Initialize Bravia TV device.""" + assert self.braviarc is not None await self.hass.async_add_executor_job( self.braviarc.connect, pin, CLIENTID_PREFIX, NICKNAME ) @@ -68,13 +74,15 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> BraviaTVOptionsFlowHandler: """Bravia TV options callback.""" return BraviaTVOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: if host_valid(user_input[CONF_HOST]): @@ -91,9 +99,11 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_authorize(self, user_input=None): + async def async_step_authorize( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Get PIN from the Bravia TV device.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: try: @@ -106,9 +116,9 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_HOST] = self.host user_input[CONF_MAC] = self.mac return self.async_create_entry(title=self.title, data=user_input) - # Connecting with th PIN "0000" to start the pairing process on the TV. try: + assert self.braviarc is not None await self.hass.async_add_executor_job( self.braviarc.connect, "0000", CLIENTID_PREFIX, NICKNAME ) @@ -125,31 +135,34 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for Bravia TV.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Bravia TV options flow.""" - self.braviarc = None self.config_entry = config_entry self.pin = config_entry.data[CONF_PIN] self.ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES) - self.source_list = [] + self.source_list: dict[str, str] = {} - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] - self.braviarc = coordinator.braviarc - connected = await self.hass.async_add_executor_job(self.braviarc.is_connected) + braviarc = coordinator.braviarc + connected = await self.hass.async_add_executor_job(braviarc.is_connected) if not connected: await self.hass.async_add_executor_job( - self.braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME + braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME ) content_mapping = await self.hass.async_add_executor_job( - self.braviarc.load_source_list + braviarc.load_source_list ) - self.source_list = [*content_mapping] + self.source_list = {item: item for item in [*content_mapping]} return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 1fa96e6a98d..01746cbe963 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -1,13 +1,17 @@ """Constants for Bravia TV integration.""" -ATTR_CID = "cid" -ATTR_MAC = "macAddr" -ATTR_MANUFACTURER = "Sony" -ATTR_MODEL = "model" +from __future__ import annotations -CONF_IGNORED_SOURCES = "ignored_sources" +from typing import Final -BRAVIA_CONFIG_FILE = "bravia.conf" -CLIENTID_PREFIX = "HomeAssistant" -DEFAULT_NAME = f"{ATTR_MANUFACTURER} Bravia TV" -DOMAIN = "braviatv" -NICKNAME = "Home Assistant" +ATTR_CID: Final = "cid" +ATTR_MAC: Final = "macAddr" +ATTR_MANUFACTURER: Final = "Sony" +ATTR_MODEL: Final = "model" + +CONF_IGNORED_SOURCES: Final = "ignored_sources" + +BRAVIA_CONFIG_FILE: Final = "bravia.conf" +CLIENTID_PREFIX: Final = "HomeAssistant" +DEFAULT_NAME: Final = f"{ATTR_MANUFACTURER} Bravia TV" +DOMAIN: Final = "braviatv" +NICKNAME: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index dda5b005497..8528e3649c1 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,4 +1,8 @@ """Support for interface with a Bravia TV.""" +from __future__ import annotations + +from typing import Final + from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -13,12 +17,17 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import BraviaTVCoordinator from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN -SUPPORT_BRAVIA = ( +SUPPORT_BRAVIA: Final = ( SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE @@ -33,12 +42,17 @@ SUPPORT_BRAVIA = ( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Bravia TV Media Player from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id - device_info = { + assert unique_id is not None + device_info: DeviceInfo = { "identifiers": {(DOMAIN, unique_id)}, "name": DEFAULT_NAME, "manufacturer": ATTR_MANUFACTURER, @@ -53,10 +67,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" + coordinator: BraviaTVCoordinator _attr_device_class = DEVICE_CLASS_TV _attr_supported_features = SUPPORT_BRAVIA - def __init__(self, coordinator, name, unique_id, device_info): + def __init__( + self, + coordinator: BraviaTVCoordinator, + name: str, + unique_id: str, + device_info: DeviceInfo, + ) -> None: """Initialize the entity.""" self._attr_device_info = device_info @@ -66,91 +87,91 @@ class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): super().__init__(coordinator) @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" if self.coordinator.is_on: return STATE_PLAYING if self.coordinator.playing else STATE_PAUSED return STATE_OFF @property - def source(self): + def source(self) -> str | None: """Return the current input source.""" return self.coordinator.source @property - def source_list(self): + def source_list(self) -> list[str]: """List of available input sources.""" return self.coordinator.source_list @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self.coordinator.volume_level @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" return self.coordinator.muted @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" return self.coordinator.media_title @property - def media_content_id(self): + def media_content_id(self) -> str | None: """Content ID of current playing media.""" return self.coordinator.channel_name @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" return self.coordinator.duration - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the device on.""" await self.coordinator.async_turn_on() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the device off.""" await self.coordinator.async_turn_off() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self.coordinator.async_set_volume_level(volume) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" await self.coordinator.async_volume_up() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command.""" await self.coordinator.async_volume_down() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self.coordinator.async_volume_mute(mute) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set the input source.""" await self.coordinator.async_select_source(source) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.coordinator.async_media_play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.coordinator.async_media_pause() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send media stop command to media player.""" await self.coordinator.async_media_stop() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self.coordinator.async_media_next_track() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.coordinator.async_media_previous_track() diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 613d67f0187..81761240320 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -1,17 +1,31 @@ """Remote control support for Bravia TV.""" +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import BraviaTVCoordinator from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Bravia TV Remote from a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id - device_info = { + assert unique_id is not None + device_info: DeviceInfo = { "identifiers": {(DOMAIN, unique_id)}, "name": DEFAULT_NAME, "manufacturer": ATTR_MANUFACTURER, @@ -26,7 +40,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BraviaTVRemote(CoordinatorEntity, RemoteEntity): """Representation of a Bravia TV Remote.""" - def __init__(self, coordinator, name, unique_id, device_info): + coordinator: BraviaTVCoordinator + + def __init__( + self, + coordinator: BraviaTVCoordinator, + name: str, + unique_id: str, + device_info: DeviceInfo, + ) -> None: """Initialize the entity.""" self._attr_device_info = device_info @@ -36,19 +58,19 @@ class BraviaTVRemote(CoordinatorEntity, RemoteEntity): super().__init__(coordinator) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.coordinator.is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.coordinator.async_turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.coordinator.async_turn_off() - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to device.""" repeats = kwargs[ATTR_NUM_REPEATS] await self.coordinator.async_send_command(command, repeats) diff --git a/mypy.ini b/mypy.ini index 09869a5b5fe..4e27da2fcb5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -209,6 +209,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.braviatv.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.brother.*] check_untyped_defs = true disallow_incomplete_defs = true From f52d838b7aedce7ddceb9cef6eaa6d9a2e302ba0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jul 2021 10:01:41 -0500 Subject: [PATCH 055/818] Bump aiohomekit to 0.4.1 (#52472) - Fixes mdns queries being sent with the original case received on the wire Some responders were case sensitive and needed the original case sent - Reduces mdns traffic --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 155f3a4f5f6..39144d6c521 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.4.0"], + "requirements": ["aiohomekit==0.4.1"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index c4be3f3a711..c69a1a3daf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.0 +aiohomekit==0.4.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b06ef05ef64..2a0f22c4d62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.0 +aiohomekit==0.4.1 # homeassistant.components.emulated_hue # homeassistant.components.http From 44b44b5bd61d65b9f4a487e8528d348c6457ade7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Jul 2021 17:06:12 +0200 Subject: [PATCH 056/818] Enable basic type checking for climate (#52470) * Enable basic type checking for climate * Tweak --- homeassistant/components/climate/__init__.py | 18 +++++++++--------- .../components/climate/device_condition.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index dbd74d1c5e8..cc26bcc9bcc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_ON, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -29,7 +29,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType, ServiceDataType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.temperature import convert as convert_temperature from .const import ( @@ -248,7 +248,7 @@ class ClimateEntity(Entity): def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" supported_features = self.supported_features - data = { + data: dict[str, str | float | None] = { ATTR_CURRENT_TEMPERATURE: show_temp( self.hass, self.current_temperature, @@ -498,7 +498,7 @@ class ClimateEntity(Entity): """Turn the entity on.""" if hasattr(self, "turn_on"): # pylint: disable=no-member - await self.hass.async_add_executor_job(self.turn_on) + await self.hass.async_add_executor_job(self.turn_on) # type: ignore[attr-defined] return # Fake turn on @@ -512,7 +512,7 @@ class ClimateEntity(Entity): """Turn the entity off.""" if hasattr(self, "turn_off"): # pylint: disable=no-member - await self.hass.async_add_executor_job(self.turn_off) + await self.hass.async_add_executor_job(self.turn_off) # type: ignore[attr-defined] return # Fake turn off @@ -554,23 +554,23 @@ class ClimateEntity(Entity): async def async_service_aux_heat( - entity: ClimateEntity, service: ServiceDataType + entity: ClimateEntity, service_call: ServiceCall ) -> None: """Handle aux heat service.""" - if service.data[ATTR_AUX_HEAT]: + if service_call.data[ATTR_AUX_HEAT]: await entity.async_turn_aux_heat_on() else: await entity.async_turn_aux_heat_off() async def async_service_temperature_set( - entity: ClimateEntity, service: ServiceDataType + entity: ClimateEntity, service_call: ServiceCall ) -> None: """Handle set temperature service.""" hass = entity.hass kwargs = {} - for value, temp in service.data.items(): + for value, temp in service_call.data.items(): if value in CONVERTIBLE_ATTRIBUTE: kwargs[value] = convert_temperature( temp, hass.config.units.temperature_unit, entity.temperature_unit diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 3fc6d94ba4f..97bb4515f14 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -85,7 +85,7 @@ def async_condition_from_config( def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" state = hass.states.get(config[ATTR_ENTITY_ID]) - return state and state.attributes.get(attribute) == config[attribute] + return state.attributes.get(attribute) == config[attribute] if state else False return test_is_state diff --git a/mypy.ini b/mypy.ini index 4e27da2fcb5..08bcedaf900 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1058,9 +1058,6 @@ ignore_errors = true [mypy-homeassistant.components.climacell.*] ignore_errors = true -[mypy-homeassistant.components.climate.*] -ignore_errors = true - [mypy-homeassistant.components.cloud.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c63033dacc8..35d38a581a7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,7 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cast.*", "homeassistant.components.cert_expiry.*", "homeassistant.components.climacell.*", - "homeassistant.components.climate.*", "homeassistant.components.cloud.*", "homeassistant.components.cloudflare.*", "homeassistant.components.config.*", From b3b377ac8bc3a8b317cfd45d6ee1a0da80624d4b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Jul 2021 17:06:42 +0200 Subject: [PATCH 057/818] Enable basic type checking for gogogate2 (#52467) * Enable basic type checking for gogogate2 * Tweak * Update homeassistant/components/gogogate2/common.py Co-authored-by: Ruslan Sayfutdinov * Tweak Co-authored-by: Ruslan Sayfutdinov --- homeassistant/components/gogogate2/__init__.py | 4 ++-- homeassistant/components/gogogate2/common.py | 6 +++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index d4271b3937a..f4ff18b0837 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -18,13 +18,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Update the config entry. config_updates = {} if CONF_DEVICE not in entry.data: - config_updates["data"] = { + config_updates = { **entry.data, **{CONF_DEVICE: DEVICE_TYPE_GOGOGATE2}, } if config_updates: - hass.config_entries.async_update_entry(entry, **config_updates) + hass.config_entries.async_update_entry(entry, data=config_updates) data_update_coordinator = get_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 5f37b135ace..c1f81f8fd32 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -1,10 +1,10 @@ """Common code for GogoGate2 component.""" from __future__ import annotations -from collections.abc import Awaitable +from collections.abc import Awaitable, Mapping from datetime import timedelta import logging -from typing import Callable, NamedTuple +from typing import Any, Callable, NamedTuple from ismartgate import AbstractGateApi, GogoGate2Api, ISmartGateApi from ismartgate.common import AbstractDoor, get_door_by_id @@ -149,7 +149,7 @@ def sensor_unique_id( return f"{config_entry.unique_id}_{door.door_id}_{sensor_type}" -def get_api(hass: HomeAssistant, config_data: dict) -> AbstractGateApi: +def get_api(hass: HomeAssistant, config_data: Mapping[str, Any]) -> AbstractGateApi: """Get an api object for config data.""" gate_class = GogoGate2Api diff --git a/mypy.ini b/mypy.ini index 08bcedaf900..1a0e6507eb6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1154,9 +1154,6 @@ ignore_errors = true [mypy-homeassistant.components.glances.*] ignore_errors = true -[mypy-homeassistant.components.gogogate2.*] -ignore_errors = true - [mypy-homeassistant.components.google_assistant.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 35d38a581a7..fef76e6d1bf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -65,7 +65,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.garmin_connect.*", "homeassistant.components.geniushub.*", "homeassistant.components.glances.*", - "homeassistant.components.gogogate2.*", "homeassistant.components.google_assistant.*", "homeassistant.components.google_maps.*", "homeassistant.components.google_pubsub.*", From 14dd6478d1ef0d437d27eb1caf79221c8adb7ffc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Jul 2021 17:52:31 +0200 Subject: [PATCH 058/818] Enable basic type checking for trace (#52468) --- homeassistant/helpers/trace.py | 6 +++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 33fe76c9eab..e25cf814b2a 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -92,7 +92,7 @@ trace_path_stack_cv: ContextVar[list[str] | None] = ContextVar( # Copy of last variables variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None) # (domain, item_id) + Run ID -trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar( +trace_id_cv: ContextVar[tuple[tuple[str, str], str] | None] = ContextVar( "trace_id_cv", default=None ) # Reason for stopped script execution @@ -101,12 +101,12 @@ script_execution_cv: ContextVar[StopReason | None] = ContextVar( ) -def trace_id_set(trace_id: tuple[str, str]) -> None: +def trace_id_set(trace_id: tuple[tuple[str, str], str]) -> None: """Set id of the current trace.""" trace_id_cv.set(trace_id) -def trace_id_get() -> tuple[str, str] | None: +def trace_id_get() -> tuple[tuple[str, str], str] | None: """Get id if the current trace.""" return trace_id_cv.get() diff --git a/mypy.ini b/mypy.ini index 1a0e6507eb6..e138db3fbd8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1568,9 +1568,6 @@ ignore_errors = true [mypy-homeassistant.components.tplink.*] ignore_errors = true -[mypy-homeassistant.components.trace.*] -ignore_errors = true - [mypy-homeassistant.components.tradfri.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index fef76e6d1bf..9a444f801bd 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -203,7 +203,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.todoist.*", "homeassistant.components.toon.*", "homeassistant.components.tplink.*", - "homeassistant.components.trace.*", "homeassistant.components.tradfri.*", "homeassistant.components.tuya.*", "homeassistant.components.unifi.*", From 513bcbc02b72c2419e4f8d754e5ae3db9119e9ca Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 3 Jul 2021 11:01:41 -0500 Subject: [PATCH 059/818] Replace custom listener with helper in SimpliSafe (#52457) --- homeassistant/components/simplisafe/__init__.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b7c2a08f093..ce30b95f82a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -41,8 +41,6 @@ from .const import ( VOLUMES, ) -DATA_LISTENER = "listener" - EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" DEFAULT_SOCKET_MIN_RETRY = 15 @@ -130,9 +128,8 @@ async def async_register_base_station(hass, system, config_entry_id): async def async_setup_entry(hass, config_entry): # noqa: C901 """Set up SimpliSafe as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}}) + hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = [] if CONF_PASSWORD not in config_entry.data: raise ConfigEntryAuthFailed("Config schema change requires re-authentication") @@ -276,9 +273,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 ]: async_register_admin_service(hass, DOMAIN, service, method, schema=schema) - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id].append( - config_entry.add_update_listener(async_reload_entry) - ) + config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) return True @@ -288,8 +283,6 @@ async def async_unload_entry(hass, entry): unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) - for remove_listener in hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id): - remove_listener() return unload_ok From 413c3afa124a5a92a3207157545198518b694d43 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 3 Jul 2021 11:16:55 -0500 Subject: [PATCH 060/818] Remove redundant property definitions in SimpliSafe (#52458) * Remove redundant property definitions in SimpliSafe * Remove useless init --- .../components/simplisafe/__init__.py | 59 ++++++---------- .../simplisafe/alarm_control_panel.py | 67 +++++++------------ .../components/simplisafe/binary_sensor.py | 34 ++-------- homeassistant/components/simplisafe/lock.py | 15 ++--- homeassistant/components/simplisafe/sensor.py | 28 +------- 5 files changed, 58 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index ce30b95f82a..fce1890b280 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -403,25 +403,25 @@ class SimpliSafeEntity(CoordinatorEntity): def __init__(self, simplisafe, system, name, *, serial=None): """Initialize.""" super().__init__(simplisafe.coordinator) - self._name = name - self._online = True - self._simplisafe = simplisafe - self._system = system if serial: self._serial = serial else: self._serial = system.serial - self._attrs = {ATTR_SYSTEM_ID: system.system_id} - - self._device_info = { + self._attr_extra_state_attributes = {ATTR_SYSTEM_ID: system.system_id} + self._attr_device_info = { "identifiers": {(DOMAIN, system.system_id)}, "manufacturer": "SimpliSafe", "model": system.version, "name": name, "via_device": (DOMAIN, system.serial), } + self._attr_name = f"{system.address} {name}" + self._attr_unique_id = self._serial + self._online = True + self._simplisafe = simplisafe + self._system = system @property def available(self): @@ -431,27 +431,11 @@ class SimpliSafeEntity(CoordinatorEntity): # the entity as available if: # 1. We can verify that the system is online (assuming True if we can't) # 2. We can verify that the entity is online - return not (self._system.version == 3 and self._system.offline) and self._online - - @property - def device_info(self): - """Return device registry information for this entity.""" - return self._device_info - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def name(self): - """Return the name of the entity.""" - return f"{self._system.address} {self._name}" - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._serial + return ( + super().available + and self._online + and not (self._system.version == 3 and self._system.offline) + ) @callback def _handle_coordinator_update(self): @@ -476,15 +460,12 @@ class SimpliSafeBaseSensor(SimpliSafeEntity): def __init__(self, simplisafe, system, sensor): """Initialize.""" super().__init__(simplisafe, system, sensor.name, serial=sensor.serial) - self._device_info["identifiers"] = {(DOMAIN, sensor.serial)} - self._device_info["model"] = sensor.type.name - self._device_info["name"] = sensor.name - self._sensor = sensor - self._sensor_type_human_name = " ".join( - [w.title() for w in self._sensor.type.name.split("_")] - ) - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._system.address} {self._name} {self._sensor_type_human_name}" + self._attr_device_info["identifiers"] = {(DOMAIN, sensor.serial)} + self._attr_device_info["model"] = sensor.type.name + self._attr_device_info["name"] = sensor.name + + human_friendly_name = " ".join([w.title() for w in sensor.type.name.split("_")]) + self._attr_name = f"{super().name} {human_friendly_name}" + + self._sensor = sensor diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 1f224683a41..28013b69d55 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -63,51 +63,32 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): def __init__(self, simplisafe, system): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") - self._changed_by = None + + if isinstance( + self._simplisafe.config_entry.options.get(CONF_CODE), str + ) and re.search("^\\d+$", self._simplisafe.config_entry.options[CONF_CODE]): + self._attr_code_format = FORMAT_NUMBER + else: + self._attr_code_format = FORMAT_TEXT + self._attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY self._last_event = None if system.alarm_going_off: - self._state = STATE_ALARM_TRIGGERED + self._attr_state = STATE_ALARM_TRIGGERED elif system.state == SystemStates.away: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY elif system.state in ( SystemStates.away_count, SystemStates.exit_delay, SystemStates.home_count, ): - self._state = STATE_ALARM_ARMING + self._attr_state = STATE_ALARM_ARMING elif system.state == SystemStates.home: - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME elif system.state == SystemStates.off: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED else: - self._state = None - - @property - def changed_by(self): - """Return info about who changed the alarm last.""" - return self._changed_by - - @property - def code_format(self): - """Return one or more digits/characters.""" - if not self._simplisafe.config_entry.options.get(CONF_CODE): - return None - if isinstance( - self._simplisafe.config_entry.options[CONF_CODE], str - ) and re.search("^\\d+$", self._simplisafe.config_entry.options[CONF_CODE]): - return FORMAT_NUMBER - return FORMAT_TEXT - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + self._attr_state = None @callback def _is_code_valid(self, code, state): @@ -134,7 +115,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): LOGGER.error('Error while disarming "%s": %s', self._system.system_id, err) return - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED self.async_write_ha_state() async def async_alarm_arm_home(self, code=None): @@ -150,7 +131,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): ) return - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME self.async_write_ha_state() async def async_alarm_arm_away(self, code=None): @@ -166,14 +147,14 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): ) return - self._state = STATE_ALARM_ARMING + self._attr_state = STATE_ALARM_ARMING self.async_write_ha_state() @callback def async_update_from_rest_api(self): """Update the entity with the provided REST API data.""" if self._system.version == 3: - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_ALARM_DURATION: self._system.alarm_duration, ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], @@ -198,14 +179,14 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): # SimpliSafe cloud can sporadically fail to send those updates as expected; so, # just in case, we synchronize the state via the REST API, too: if self._system.state == SystemStates.alarm: - self._state = STATE_ALARM_TRIGGERED + self._attr_state = STATE_ALARM_TRIGGERED elif self._system.state == SystemStates.away: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY elif self._system.state in (SystemStates.away_count, SystemStates.exit_delay): - self._state = STATE_ALARM_ARMING + self._attr_state = STATE_ALARM_ARMING elif self._system.state == SystemStates.home: - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME elif self._system.state == SystemStates.off: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED else: - self._state = None + self._attr_state = None diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index a92309c1123..70fa0fb0e51 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -71,49 +71,27 @@ class TriggeredBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): def __init__(self, simplisafe, system, sensor, device_class): """Initialize.""" super().__init__(simplisafe, system, sensor) - self._device_class = device_class - self._is_on = False - @property - def device_class(self): - """Return type of sensor.""" - return self._device_class - - @property - def is_on(self): - """Return true if the sensor is on.""" - return self._is_on + self._attr_device_class = device_class @callback def async_update_from_rest_api(self): """Update the entity with the provided REST API data.""" - self._is_on = self._sensor.triggered + self._attr_is_on = self._sensor.triggered class BatteryBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): """Define a SimpliSafe battery binary sensor entity.""" + _attr_device_class = DEVICE_CLASS_BATTERY + def __init__(self, simplisafe, system, sensor): """Initialize.""" super().__init__(simplisafe, system, sensor) - self._is_low = False - @property - def device_class(self): - """Return type of sensor.""" - return DEVICE_CLASS_BATTERY - - @property - def unique_id(self): - """Return unique ID of sensor.""" - return f"{self._sensor.serial}-battery" - - @property - def is_on(self): - """Return true if the battery is low.""" - return self._is_low + self._attr_unique_id = f"{super().unique_id}-battery" @callback def async_update_from_rest_api(self): """Update the entity with the provided REST API data.""" - self._is_low = self._sensor.low_battery + self._attr_is_on = self._sensor.low_battery diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 8bfda08c1a5..982494d9b1c 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -35,13 +35,8 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): def __init__(self, simplisafe, system, lock): """Initialize.""" super().__init__(simplisafe, system, lock.name, serial=lock.serial) - self._lock = lock - self._is_locked = None - @property - def is_locked(self): - """Return true if the lock is locked.""" - return self._is_locked + self._lock = lock async def async_lock(self, **kwargs): """Lock the lock.""" @@ -51,7 +46,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): LOGGER.error('Error while locking "%s": %s', self._lock.name, err) return - self._is_locked = True + self._attr_is_locked = True self.async_write_ha_state() async def async_unlock(self, **kwargs): @@ -62,13 +57,13 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) return - self._is_locked = False + self._attr_is_locked = False self.async_write_ha_state() @callback def async_update_from_rest_api(self): """Update the entity with the provided REST API data.""" - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, ATTR_JAMMED: self._lock.state == LockStates.jammed, @@ -76,4 +71,4 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): } ) - self._is_locked = self._lock.state == LockStates.locked + self._attr_is_locked = self._lock.state == LockStates.locked diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 9f93a6f9e87..76be7b6e4f0 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -29,32 +29,10 @@ async def async_setup_entry(hass, entry, async_add_entities): class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): """Define a SimpliSafe freeze sensor entity.""" - def __init__(self, simplisafe, system, sensor): - """Initialize.""" - super().__init__(simplisafe, system, sensor) - self._state = None - - @property - def device_class(self): - """Return type of sensor.""" - return DEVICE_CLASS_TEMPERATURE - - @property - def unique_id(self): - """Return unique ID of sensor.""" - return self._sensor.serial - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def state(self): - """Return the sensor state.""" - return self._state + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_FAHRENHEIT @callback def async_update_from_rest_api(self): """Update the entity with the provided REST API data.""" - self._state = self._sensor.temperature + self._attr_state = self._sensor.temperature From 8c0559cc57fae80d4815b2e96f7dc57b7a3ae236 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 3 Jul 2021 11:23:52 -0500 Subject: [PATCH 061/818] Remove redundant property definitions in RainMachine (#52456) * Remove redundant property definitions in RainMachine * Incorrect attribute name --- .../components/rainmachine/__init__.py | 57 ++++++---------- .../components/rainmachine/binary_sensor.py | 65 +++++++------------ .../components/rainmachine/sensor.py | 62 ++++++------------ .../components/rainmachine/switch.py | 38 +++++------ 4 files changed, 77 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 09af357617d..6a239bc84a1 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -19,7 +19,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -174,48 +173,32 @@ class RainMachineEntity(CoordinatorEntity): """Define a generic RainMachine entity.""" def __init__( - self, coordinator: DataUpdateCoordinator, controller: Controller + self, + coordinator: DataUpdateCoordinator, + controller: Controller, + entity_type: str, ) -> None: """Initialize.""" super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._controller = controller - self._device_class = None + + self._attr_device_info = { + "identifiers": {(DOMAIN, controller.mac)}, + "connections": {(dr.CONNECTION_NETWORK_MAC, controller.mac)}, + "name": controller.name, + "manufacturer": "RainMachine", + "model": ( + f"Version {controller.hardware_version} " + f"(API: {controller.api_version})" + ), + "sw_version": controller.software_version, + } + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} # The colons are removed from the device MAC simply because that value # (unnecessarily) makes up the existing unique ID formula and we want to avoid # a breaking change: - self._unique_id = controller.mac.replace(":", "") - self._name = None - - @property - def device_class(self) -> str: - """Return the device class.""" - return self._device_class - - @property - def device_info(self) -> DeviceInfo: - """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._controller.mac)}, - "connections": {(dr.CONNECTION_NETWORK_MAC, self._controller.mac)}, - "name": self._controller.name, - "manufacturer": "RainMachine", - "model": ( - f"Version {self._controller.hardware_version} " - f"(API: {self._controller.api_version})" - ), - "sw_version": self._controller.software_version, - } - - @property - def extra_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name + self._attr_unique_id = f"{controller.mac.replace(':', '')}_{entity_type}" + self._controller = controller + self._entity_type = entity_type @callback def _handle_coordinator_update(self): diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 171fd26b910..b666d9ed150 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -125,32 +125,11 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity): enabled_by_default: bool, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, controller) - self._enabled_by_default = enabled_by_default - self._icon = icon - self._name = name - self._sensor_type = sensor_type - self._state = None + super().__init__(coordinator, controller, sensor_type) - @property - def entity_registry_enabled_default(self) -> bool: - """Determine whether an entity is enabled by default.""" - return self._enabled_by_default - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def is_on(self) -> bool: - """Return the status of the sensor.""" - return self._state - - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._unique_id}_{self._sensor_type}" + self._attr_entity_registry_enabled_default = enabled_by_default + self._attr_icon = icon + self._attr_name = name class CurrentRestrictionsBinarySensor(RainMachineBinarySensor): @@ -159,18 +138,18 @@ class CurrentRestrictionsBinarySensor(RainMachineBinarySensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FREEZE: - self._state = self.coordinator.data["freeze"] - elif self._sensor_type == TYPE_HOURLY: - self._state = self.coordinator.data["hourly"] - elif self._sensor_type == TYPE_MONTH: - self._state = self.coordinator.data["month"] - elif self._sensor_type == TYPE_RAINDELAY: - self._state = self.coordinator.data["rainDelay"] - elif self._sensor_type == TYPE_RAINSENSOR: - self._state = self.coordinator.data["rainSensor"] - elif self._sensor_type == TYPE_WEEKDAY: - self._state = self.coordinator.data["weekDay"] + if self._entity_type == TYPE_FREEZE: + self._attr_is_on = self.coordinator.data["freeze"] + elif self._entity_type == TYPE_HOURLY: + self._attr_is_on = self.coordinator.data["hourly"] + elif self._entity_type == TYPE_MONTH: + self._attr_is_on = self.coordinator.data["month"] + elif self._entity_type == TYPE_RAINDELAY: + self._attr_is_on = self.coordinator.data["rainDelay"] + elif self._entity_type == TYPE_RAINSENSOR: + self._attr_is_on = self.coordinator.data["rainSensor"] + elif self._entity_type == TYPE_WEEKDAY: + self._attr_is_on = self.coordinator.data["weekDay"] class ProvisionSettingsBinarySensor(RainMachineBinarySensor): @@ -179,8 +158,8 @@ class ProvisionSettingsBinarySensor(RainMachineBinarySensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FLOW_SENSOR: - self._state = self.coordinator.data["system"].get("useFlowSensor") + if self._entity_type == TYPE_FLOW_SENSOR: + self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor") class UniversalRestrictionsBinarySensor(RainMachineBinarySensor): @@ -189,7 +168,7 @@ class UniversalRestrictionsBinarySensor(RainMachineBinarySensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FREEZE_PROTECTION: - self._state = self.coordinator.data["freezeProtectEnabled"] - elif self._sensor_type == TYPE_HOT_DAYS: - self._state = self.coordinator.data["hotDaysExtraWatering"] + if self._entity_type == TYPE_FREEZE_PROTECTION: + self._attr_is_on = self.coordinator.data["freezeProtectEnabled"] + elif self._entity_type == TYPE_HOT_DAYS: + self._attr_is_on = self.coordinator.data["hotDaysExtraWatering"] diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 2ebd9d0fdb4..87feb85046d 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -124,39 +124,13 @@ class RainMachineSensor(RainMachineEntity, SensorEntity): enabled_by_default: bool, ) -> None: """Initialize.""" - super().__init__(coordinator, controller) - self._device_class = device_class - self._enabled_by_default = enabled_by_default - self._icon = icon - self._name = name - self._sensor_type = sensor_type - self._state = None - self._unit = unit + super().__init__(coordinator, controller, sensor_type) - @property - def entity_registry_enabled_default(self) -> bool: - """Determine whether an entity is enabled by default.""" - return self._enabled_by_default - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def state(self) -> str: - """Return the name of the entity.""" - return self._state - - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._unique_id}_{self._sensor_type}" - - @property - def unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - return self._unit + self._attr_device_class = device_class + self._attr_entity_registry_enabled_default = enabled_by_default + self._attr_icon = icon + self._attr_name = name + self._attr_unit_of_measurement = unit class ProvisionSettingsSensor(RainMachineSensor): @@ -165,24 +139,26 @@ class ProvisionSettingsSensor(RainMachineSensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3: - self._state = self.coordinator.data["system"].get( + if self._entity_type == TYPE_FLOW_SENSOR_CLICK_M3: + self._attr_state = self.coordinator.data["system"].get( "flowSensorClicksPerCubicMeter" ) - elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: + elif self._entity_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: clicks = self.coordinator.data["system"].get("flowSensorWateringClicks") clicks_per_m3 = self.coordinator.data["system"].get( "flowSensorClicksPerCubicMeter" ) if clicks and clicks_per_m3: - self._state = (clicks * 1000) / clicks_per_m3 + self._attr_state = (clicks * 1000) / clicks_per_m3 else: - self._state = None - elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX: - self._state = self.coordinator.data["system"].get("flowSensorStartIndex") - elif self._sensor_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: - self._state = self.coordinator.data["system"].get( + self._attr_state = None + elif self._entity_type == TYPE_FLOW_SENSOR_START_INDEX: + self._attr_state = self.coordinator.data["system"].get( + "flowSensorStartIndex" + ) + elif self._entity_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: + self._attr_state = self.coordinator.data["system"].get( "flowSensorWateringClicks" ) @@ -193,5 +169,5 @@ class UniversalRestrictionsSensor(RainMachineSensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FREEZE_TEMP: - self._state = self.coordinator.data["freezeProtectTemp"] + if self._entity_type == TYPE_FREEZE_TEMP: + self._attr_state = self.coordinator.data["freezeProtectTemp"] diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index b0544b1adbe..68ecd8c8d64 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -53,6 +53,8 @@ CONF_PROGRAM_ID = "program_id" CONF_SECONDS = "seconds" CONF_ZONE_ID = "zone_id" +DEFAULT_ICON = "mdi:water" + DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"} @@ -181,6 +183,8 @@ async def async_setup_entry( class RainMachineSwitch(RainMachineEntity, SwitchEntity): """A class to represent a generic RainMachine switch.""" + _attr_icon = DEFAULT_ICON + def __init__( self, coordinator: DataUpdateCoordinator, @@ -190,34 +194,24 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): entry: ConfigEntry, ) -> None: """Initialize a generic RainMachine switch.""" - super().__init__(coordinator, controller) + super().__init__(coordinator, controller, type(self).__name__) + + self._attr_is_on = False + self._attr_name = name self._data = coordinator.data[uid] self._entry = entry self._is_active = True - self._is_on = False - self._name = name - self._switch_type = type(self).__name__ self._uid = uid @property def available(self) -> bool: """Return True if entity is available.""" - return self._is_active and self.coordinator.last_update_success - - @property - def icon(self) -> str: - """Return the icon.""" - return "mdi:water" - - @property - def is_on(self) -> bool: - """Return whether the program is running.""" - return self._is_on + return super().available and self._is_active @property def unique_id(self) -> str: """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._unique_id}_{self._switch_type}_{self._uid}" + return f"{super().unique_id}_{self._uid}" async def _async_run_switch_coroutine(self, api_coro: Coroutine) -> None: """Run a coroutine to toggle the switch.""" @@ -226,7 +220,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): except RequestError as err: LOGGER.error( 'Error while toggling %s "%s": %s', - self._switch_type, + self._entity_type, self.unique_id, err, ) @@ -235,7 +229,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): if resp["statusCode"] != 0: LOGGER.error( 'Error while toggling %s "%s": %s', - self._switch_type, + self._entity_type, self.unique_id, resp["message"], ) @@ -334,7 +328,7 @@ class RainMachineProgram(RainMachineSwitch): """Update the state.""" super().update_from_latest_data() - self._is_on = bool(self._data["status"]) + self._attr_is_on = bool(self._data["status"]) if self._data.get("nextRun") is not None: next_run = datetime.strptime( @@ -344,7 +338,7 @@ class RainMachineProgram(RainMachineSwitch): else: next_run = None - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_ID: self._uid, ATTR_NEXT_RUN: next_run, @@ -376,9 +370,9 @@ class RainMachineZone(RainMachineSwitch): """Update the state.""" super().update_from_latest_data() - self._is_on = bool(self._data["state"]) + self._attr_is_on = bool(self._data["state"]) - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_STATUS: RUN_STATUS_MAP[self._data["state"]], ATTR_AREA: self._data.get("waterSense").get("area"), From e29c75a68e398e3734e0ff55e785632234cade7b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 3 Jul 2021 11:29:52 -0500 Subject: [PATCH 062/818] Remove redundant property definitions in Tile (#52448) --- homeassistant/components/tile/__init__.py | 7 +--- .../components/tile/device_tracker.py | 39 ++++--------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 7faefe4d275..d8a592a2f09 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -24,14 +24,9 @@ DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" -async def async_setup(hass, config): - """Set up the Tile component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_TILE: {}} - return True - - async def async_setup_entry(hass, entry): """Set up Tile as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_TILE: {}}) hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index add6e5f94a0..ea1b853d9ae 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -63,35 +63,22 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): class TileDeviceTracker(CoordinatorEntity, TrackerEntity): """Representation of a network infrastructure device.""" + _attr_icon = DEFAULT_ICON + def __init__(self, entry, coordinator, tile): """Initialize.""" super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_name = tile.name + self._attr_unique_id = f"{entry.data[CONF_USERNAME]}_{tile.uuid}" self._entry = entry self._tile = tile @property def available(self): """Return if entity is available.""" - return self.coordinator.last_update_success and not self._tile.dead - - @property - def battery_level(self): - """Return the battery level of the device. - - Percentage from 0-100. - """ - return None - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return DEFAULT_ICON + return super().available and not self._tile.dead @property def location_accuracy(self): @@ -111,16 +98,6 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): """Return longitude value of the device.""" return self._tile.longitude - @property - def name(self): - """Return the name.""" - return self._tile.name - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._entry.data[CONF_USERNAME]}_{self._tile.uuid}" - @property def source_type(self): """Return the source type, eg gps or router, of the device.""" @@ -135,7 +112,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): @callback def _update_from_latest_data(self): """Update the entity from the latest data.""" - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_ALTITUDE: self._tile.altitude, ATTR_IS_LOST: self._tile.lost, From 628eafaf6898c5ba4e1b922cd7c28641f5a6c672 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Jul 2021 18:35:36 +0200 Subject: [PATCH 063/818] Enable basic type checking for script (#52476) --- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 2 files changed, 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index e138db3fbd8..78a42cdaaf4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1469,9 +1469,6 @@ ignore_errors = true [mypy-homeassistant.components.screenlogic.*] ignore_errors = true -[mypy-homeassistant.components.script.*] -ignore_errors = true - [mypy-homeassistant.components.search.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9a444f801bd..d5c28b0cc3c 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -170,7 +170,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.ruckus_unleashed.*", "homeassistant.components.sabnzbd.*", "homeassistant.components.screenlogic.*", - "homeassistant.components.script.*", "homeassistant.components.search.*", "homeassistant.components.sense.*", "homeassistant.components.sesame.*", From d980dd8d59795c890e8964825c51d2c1a1811039 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 3 Jul 2021 14:45:51 -0500 Subject: [PATCH 064/818] Deprecate YAML config for Ambient PWs (2021.9.0 removal) (#52459) --- .../components/ambient_station/__init__.py | 39 ++----------------- .../components/ambient_station/config_flow.py | 4 -- .../ambient_station/test_config_flow.py | 21 ---------- 3 files changed, 3 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 347ba700060..f276597c79a 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -2,14 +2,12 @@ from aioambient import Client from aioambient.errors import WebsocketError -import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DOMAIN as BINARY_SENSOR, ) from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_LOCATION, ATTR_NAME, @@ -291,44 +289,13 @@ SENSOR_TYPES = { TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None), } -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_APP_KEY): cv.string, - vol.Required(CONF_API_KEY): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the Ambient PWS integration.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} - - if DOMAIN not in config: - return True - conf = config[DOMAIN] - - # Store config for use during entry setup: - hass.data[DOMAIN][DATA_CONFIG] = conf - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_API_KEY: conf[CONF_API_KEY], CONF_APP_KEY: conf[CONF_APP_KEY]}, - ) - ) - - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass, config_entry): """Set up the Ambient PWS as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) + if not config_entry.unique_id: hass.config_entries.async_update_entry( config_entry, unique_id=config_entry.data[CONF_APP_KEY] diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 429388dcaba..5b40300498b 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -29,10 +29,6 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) - async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" if not user_input: diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index a64b7761338..806d31b5386 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -83,27 +83,6 @@ async def test_show_form(hass): assert result["step_id"] == "user" -@pytest.mark.parametrize( - "get_devices_response", - [mock_coro(return_value=json.loads(load_fixture("ambient_devices.json")))], -) -async def test_step_import(hass, mock_aioambient): - """Test that the import step works.""" - conf = {CONF_API_KEY: "12345abcde12345abcde", CONF_APP_KEY: "67890fghij67890fghij"} - - flow = config_flow.AmbientStationFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} - - result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "67890fghij67" - assert result["data"] == { - CONF_API_KEY: "12345abcde12345abcde", - CONF_APP_KEY: "67890fghij67890fghij", - } - - @pytest.mark.parametrize( "get_devices_response", [mock_coro(return_value=json.loads(load_fixture("ambient_devices.json")))], From 378b5f75ec2ccdcc3c78bcc1f1323c0b759877bb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 4 Jul 2021 00:09:33 +0000 Subject: [PATCH 065/818] [ci skip] Translation update --- .../accuweather/translations/ar.json | 9 ++++ .../alarm_control_panel/translations/ar.json | 20 ++++++++- .../alarmdecoder/translations/ar.json | 41 +++++++++++++++++++ .../components/apple_tv/translations/ar.json | 10 +++++ .../components/coinbase/translations/ar.json | 17 ++++++++ .../components/deconz/translations/ar.json | 23 +++++++++++ .../components/denonavr/translations/ar.json | 7 ++++ .../components/ezviz/translations/ar.json | 7 ++++ .../forecast_solar/translations/ca.json | 2 +- .../freedompro/translations/ar.json | 13 ++++++ .../components/gree/translations/ar.json | 13 ++++++ .../homematicip_cloud/translations/ar.json | 7 ++++ .../components/hue/translations/ar.json | 20 +++++++++ .../components/konnected/translations/ar.json | 11 +++++ .../components/motioneye/translations/ar.json | 12 ++++++ .../components/mqtt/translations/ar.json | 9 ++++ .../components/nest/translations/ar.json | 21 ++++++++++ .../nmap_tracker/translations/ar.json | 12 ++++++ .../nmap_tracker/translations/ca.json | 4 +- .../nmap_tracker/translations/et.json | 4 +- .../nmap_tracker/translations/ru.json | 4 +- .../nmap_tracker/translations/zh-Hant.json | 4 +- .../components/onvif/translations/ar.json | 21 ++++++++++ .../components/risco/translations/ar.json | 12 ++++++ .../components/shelly/translations/ar.json | 9 ++++ .../components/sia/translations/ar.json | 10 +++++ .../components/sonos/translations/ar.json | 9 ++++ .../components/toon/translations/ar.json | 17 ++++++++ .../wolflink/translations/sensor.ar.json | 7 ++++ .../components/zha/translations/ar.json | 7 ++++ .../components/zone/translations/ar.json | 20 +++++++++ 31 files changed, 376 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/ar.json create mode 100644 homeassistant/components/alarmdecoder/translations/ar.json create mode 100644 homeassistant/components/apple_tv/translations/ar.json create mode 100644 homeassistant/components/coinbase/translations/ar.json create mode 100644 homeassistant/components/deconz/translations/ar.json create mode 100644 homeassistant/components/denonavr/translations/ar.json create mode 100644 homeassistant/components/ezviz/translations/ar.json create mode 100644 homeassistant/components/freedompro/translations/ar.json create mode 100644 homeassistant/components/gree/translations/ar.json create mode 100644 homeassistant/components/homematicip_cloud/translations/ar.json create mode 100644 homeassistant/components/hue/translations/ar.json create mode 100644 homeassistant/components/konnected/translations/ar.json create mode 100644 homeassistant/components/motioneye/translations/ar.json create mode 100644 homeassistant/components/mqtt/translations/ar.json create mode 100644 homeassistant/components/nest/translations/ar.json create mode 100644 homeassistant/components/nmap_tracker/translations/ar.json create mode 100644 homeassistant/components/onvif/translations/ar.json create mode 100644 homeassistant/components/risco/translations/ar.json create mode 100644 homeassistant/components/shelly/translations/ar.json create mode 100644 homeassistant/components/sia/translations/ar.json create mode 100644 homeassistant/components/sonos/translations/ar.json create mode 100644 homeassistant/components/toon/translations/ar.json create mode 100644 homeassistant/components/wolflink/translations/sensor.ar.json create mode 100644 homeassistant/components/zha/translations/ar.json create mode 100644 homeassistant/components/zone/translations/ar.json diff --git a/homeassistant/components/accuweather/translations/ar.json b/homeassistant/components/accuweather/translations/ar.json new file mode 100644 index 00000000000..bd0c7cf77ba --- /dev/null +++ b/homeassistant/components/accuweather/translations/ar.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "user": { + "description": "\u0646\u0638\u0631\u064b\u0627 \u0644\u0642\u064a\u0648\u062f \u0627\u0644\u0625\u0635\u062f\u0627\u0631 \u0627\u0644\u0645\u062c\u0627\u0646\u064a \u0645\u0646 \u0645\u0641\u062a\u0627\u062d AccuWeather API \u060c \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0646\u0628\u0624 \u0628\u0627\u0644\u0637\u0642\u0633 \u060c \u0633\u064a\u062a\u0645 \u0625\u062c\u0631\u0627\u0621 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0643\u0644 80 \u062f\u0642\u064a\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0643\u0644 40 \u062f\u0642\u064a\u0642\u0629." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/ar.json b/homeassistant/components/alarm_control_panel/translations/ar.json index 427b30eebbe..66857391c28 100644 --- a/homeassistant/components/alarm_control_panel/translations/ar.json +++ b/homeassistant/components/alarm_control_panel/translations/ar.json @@ -1,11 +1,29 @@ { + "device_automation": { + "action_type": { + "disarm": "\u0627\u0644\u063a\u064a \u062a\u0641\u0639\u064a\u0644 {entity_name}", + "trigger": "\u062a\u0634\u063a\u064a\u0644 {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u0645\u0641\u0639\u0644 \u0628\u0639\u064a\u062f\u0627", + "is_armed_home": "{entity_name} \u0645\u0641\u0639\u0644 \u0628\u0627\u0644\u0645\u0646\u0632\u0644", + "is_disarmed": "{entity_name} \u0627\u0644\u063a\u064a \u0627\u0644\u062a\u0641\u0639\u064a\u0644", + "is_triggered": "\u062a\u0645 \u062a\u0634\u063a\u064a\u0644 {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u0645\u0641\u0639\u0644 \u0628\u0639\u064a\u062f\u0627", + "armed_home": "{entity_name} \u0645\u0641\u0639\u0644 \u0628\u0627\u0644\u0645\u0646\u0632\u0644", + "disarmed": "{entity_name} \u0627\u0644\u063a\u064a \u0627\u0644\u062a\u0641\u0639\u064a\u0644" + } + }, "state": { "_": { - "armed": "\u0645\u0633\u0644\u062d", + "armed": "\u0645\u0641\u0639\u0651\u0644", "armed_away": "\u0645\u0641\u0639\u0651\u0644 \u0641\u064a \u0627\u0644\u062e\u0627\u0631\u062c", "armed_custom_bypass": "\u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u062a\u0641\u0639\u064a\u0644", "armed_home": "\u0645\u0641\u0639\u0651\u0644 \u0641\u064a \u0627\u0644\u0645\u0646\u0632\u0644", "armed_night": "\u0645\u0641\u0639\u0651\u0644 \u0644\u064a\u0644", + "armed_vacation": "\u0645\u0641\u0639\u0644 \u0628\u0648\u0636\u0639 \u0627\u0644\u0627\u062c\u0627\u0632\u0629", "arming": "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0641\u0639\u064a\u0644", "disarmed": "\u063a\u064a\u0631 \u0645\u0641\u0639\u0651\u0644", "disarming": "\u0625\u064a\u0642\u0627\u0641 \u0627\u0644\u0625\u0646\u0630\u0627\u0631", diff --git a/homeassistant/components/alarmdecoder/translations/ar.json b/homeassistant/components/alarmdecoder/translations/ar.json new file mode 100644 index 00000000000..1f49f618afb --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/ar.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "protocol": { + "data": { + "device_baudrate": "\u0645\u0639\u062f\u0644 \u0633\u0631\u0639\u0629 \u0627\u0644\u0628\u062b \u0644\u0644\u062c\u0647\u0627\u0632", + "device_path": "\u0645\u0633\u0627\u0631 \u0627\u0644\u062c\u0647\u0627\u0632" + }, + "title": "\u062a\u0643\u0648\u064a\u0646 \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "user": { + "data": { + "protocol": "\u0628\u0631\u0648\u062a\u0648\u0643\u0648\u0644" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "edit_select": "\u062a\u0639\u062f\u064a\u0644" + }, + "description": "\u0645\u0627 \u0627\u0644\u0630\u064a \u062a\u0631\u064a\u062f \u062a\u0639\u062f\u064a\u0644\u0647\u061f", + "title": "\u062a\u0643\u0648\u064a\u0646 AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_name": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0646\u0637\u0642\u0629", + "zone_type": "\u0646\u0648\u0639 \u0627\u0644\u0645\u0646\u0637\u0642\u0629" + }, + "title": "\u062a\u0643\u0648\u064a\u0646 AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u0631\u0642\u0645 \u0627\u0644\u0645\u0646\u0637\u0642\u0629" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ar.json b/homeassistant/components/apple_tv/translations/ar.json new file mode 100644 index 00000000000..07ffa860c9c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ar.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reconfigure": { + "description": "\u064a\u0648\u0627\u062c\u0647 Apple TV \u0647\u0630\u0627 \u0628\u0639\u0636 \u0627\u0644\u0635\u0639\u0648\u0628\u0627\u062a \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0648\u064a\u062c\u0628 \u0625\u0639\u0627\u062f\u0629 \u062a\u0643\u0648\u064a\u0646\u0647.", + "title": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062c\u0647\u0627\u0632" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/ar.json b/homeassistant/components/coinbase/translations/ar.json new file mode 100644 index 00000000000..026b7882503 --- /dev/null +++ b/homeassistant/components/coinbase/translations/ar.json @@ -0,0 +1,17 @@ +{ + "options": { + "error": { + "exchange_rate_unavaliable": "\u0644\u0627 \u064a\u062a\u0645 \u062a\u0648\u0641\u064a\u0631 \u0648\u0627\u062d\u062f \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641 \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0645\u0646 Coinbase.", + "unknown": "\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "\u0623\u0631\u0635\u062f\u0629 \u0627\u0644\u0645\u062d\u0641\u0638\u0629 \u0644\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646\u0647\u0627.", + "exchange_rate_currencies": "\u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641 \u0644\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646\u0647\u0627." + }, + "description": "\u0636\u0628\u0637 \u062e\u064a\u0627\u0631\u0627\u062a Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ar.json b/homeassistant/components/deconz/translations/ar.json new file mode 100644 index 00000000000..9624f9c47c9 --- /dev/null +++ b/homeassistant/components/deconz/translations/ar.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0633\u0631 \u062a\u0645 \u062a\u0643\u0648\u064a\u0646\u0647 \u0645\u0633\u0628\u0642\u0627", + "no_bridges": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0643\u062a\u0634\u0627\u0641 \u062c\u0633\u0648\u0631 deCONZ" + }, + "error": { + "no_key": "\u062a\u0639\u0630\u0631 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0645\u0641\u062a\u0627\u062d API" + }, + "step": { + "link": { + "description": "\u0627\u0641\u062a\u062d \u0628\u0648\u0627\u0628\u0629 deCONZ \u0644\u0644\u062a\u0633\u062c\u064a\u0644 \u0641\u064a Home Assistant. \n\n 1. \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649 \u0625\u0639\u062f\u0627\u062f\u0627\u062a deCONZ - > \u0627\u0644\u0628\u0648\u0627\u0628\u0629 - > \u062e\u064a\u0627\u0631\u0627\u062a \u0645\u062a\u0642\u062f\u0645\u0629\n 2. \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0632\u0631 \"\u0645\u0635\u0627\u062f\u0642\u0629 \u0627\u0644\u062a\u0637\u0628\u064a\u0642\"", + "title": "\u0627\u0644\u0627\u0631\u062a\u0628\u0627\u0637 \u0645\u0639 deCONZ" + } + } + }, + "device_automation": { + "trigger_type": { + "remote_flip_180_degrees": "\u0627\u0646\u0642\u0644\u0628 \u0627\u0644\u062c\u0647\u0627\u0632 180 \u062f\u0631\u062c\u0629", + "remote_flip_90_degrees": "\u0627\u0646\u0642\u0644\u0628 \u0627\u0644\u062c\u0647\u0627\u0632 90 \u062f\u0631\u062c\u0629" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ar.json b/homeassistant/components/denonavr/translations/ar.json new file mode 100644 index 00000000000..18369967b90 --- /dev/null +++ b/homeassistant/components/denonavr/translations/ar.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u0641\u0634\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644\u060c \u0627\u0644\u0631\u062c\u0627\u0621 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649\u060c \u0642\u062f \u064a\u0633\u0627\u0639\u062f \u0642\u0637\u0639 \u0627\u0644\u062a\u064a\u0627\u0631 \u0627\u0644\u0643\u0647\u0631\u0628\u0627\u0626\u064a \u0648\u0643\u0627\u0628\u0644\u0627\u062a \u0625\u064a\u062b\u0631\u0646\u062a \u0648\u0625\u0639\u0627\u062f\u0629 \u062a\u0648\u0635\u064a\u0644\u0647\u0627" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ar.json b/homeassistant/components/ezviz/translations/ar.json new file mode 100644 index 00000000000..4ebc8494679 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ar.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "ezviz_cloud_account_missing": "\u062d\u0633\u0627\u0628 \u0633\u062d\u0627\u0628\u0629 Ezviz \u0645\u0641\u0642\u0648\u062f. \u064a\u0631\u062c\u0649 \u0625\u0639\u0627\u062f\u0629 \u062a\u0643\u0648\u064a\u0646 \u062d\u0633\u0627\u0628 \u0633\u062d\u0627\u0628\u0629 Ezviz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/ca.json b/homeassistant/components/forecast_solar/translations/ca.json index 66e29caedb1..7bd31828080 100644 --- a/homeassistant/components/forecast_solar/translations/ca.json +++ b/homeassistant/components/forecast_solar/translations/ca.json @@ -24,7 +24,7 @@ "declination": "Inclinaci\u00f3 (0 = horitzontal, 90 = vertical)", "modules power": "Pot\u00e8ncia m\u00e0xima total dels panells solars" }, - "description": "Aquests valors permeten ajustar els resultats de Solar.Forecast. Consulta la documentaci\u00f3 si tens dubtes en algun camp." + "description": "Aquests valors permeten ajustar els resultats de Solar.Forecast. Consulta la documentaci\u00f3 si tens dubtes sobre algun camp." } } } diff --git a/homeassistant/components/freedompro/translations/ar.json b/homeassistant/components/freedompro/translations/ar.json new file mode 100644 index 00000000000..87f48ee498d --- /dev/null +++ b/homeassistant/components/freedompro/translations/ar.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "\u0645\u0641\u062a\u0627\u062d API" + }, + "description": "\u0627\u0644\u0631\u062c\u0627\u0621 \u0625\u062f\u062e\u0627\u0644 \u0645\u0641\u062a\u0627\u062d API \u0627\u0644\u0630\u064a \u062a\u0645 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u064a\u0647 \u0645\u0646 https://home.freedompro.eu", + "title": "\u0645\u0641\u062a\u0627\u062d Freedompro API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/ar.json b/homeassistant/components/gree/translations/ar.json new file mode 100644 index 00000000000..205a46af479 --- /dev/null +++ b/homeassistant/components/gree/translations/ar.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0643\u062a\u0634\u0627\u0641 \u0627\u062c\u0647\u0632\u0629 \u0641\u064a \u0645\u0646\u0632\u0644\u0643", + "single_instance_allowed": "\u0633\u0628\u0642 \u0648\u062a\u0645 \u062a\u0643\u0648\u064a\u0646\u0647. \u0641\u0642\u0637 \u062a\u0643\u0648\u064a\u0646 \u0648\u0627\u062d\u062f \u0645\u0645\u0643\u0646." + }, + "step": { + "confirm": { + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0628\u062f\u0621 \u0627\u0644\u0636\u0628\u0637\u061f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ar.json b/homeassistant/components/homematicip_cloud/translations/ar.json new file mode 100644 index 00000000000..5685fa738c7 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/ar.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "register_failed": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ar.json b/homeassistant/components/hue/translations/ar.json new file mode 100644 index 00000000000..785292b5729 --- /dev/null +++ b/homeassistant/components/hue/translations/ar.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "all_configured": "\u062a\u0645 \u062a\u0643\u0648\u064a\u0646 \u062c\u0645\u064a\u0639 \u062c\u0633\u0648\u0631 Philips Hue \u0645\u0633\u0628\u0642\u0627", + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "discover_timeout": "\u063a\u064a\u0631 \u0642\u0627\u062f\u0631 \u0639\u0644\u0649 \u0627\u0643\u062a\u0634\u0627\u0641 \u062c\u0633\u0648\u0631 Hue", + "no_bridges": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0643\u062a\u0634\u0627\u0641 \u062c\u0633\u0648\u0631 Philips Hue", + "unknown": "\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "error": { + "linking": "\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639", + "register_failed": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649" + }, + "step": { + "link": { + "description": "\u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0627\u0644\u0632\u0631 \u0627\u0644\u0645\u0648\u062c\u0648\u062f \u0639\u0644\u0649 \u0627\u0644\u062c\u0633\u0631 \u0644\u062a\u0633\u062c\u064a\u0644 Philips Hue \u0645\u0639 Home Assistant. \n\n ! [\u0645\u0648\u0642\u0639 \u0627\u0644\u0632\u0631 \u0639\u0644\u0649 \u0627\u0644\u062c\u0633\u0631] (/static/images/config_philips_hue.jpg)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ar.json b/homeassistant/components/konnected/translations/ar.json new file mode 100644 index 00000000000..9b79d82eaff --- /dev/null +++ b/homeassistant/components/konnected/translations/ar.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "options_io_ext": { + "data": { + "alarm1": "\u0625\u0646\u0630\u0627\u0631 1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ar.json b/homeassistant/components/motioneye/translations/ar.json new file mode 100644 index 00000000000..c4e4b0f397a --- /dev/null +++ b/homeassistant/components/motioneye/translations/ar.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "webhook_set": "\u0642\u0645 \u0628\u062a\u0643\u0648\u064a\u0646 webhooks \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0640 motionEye \u0644\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646 \u0627\u0644\u0623\u062d\u062f\u0627\u062b \u0644\u0640 Home Assistant", + "webhook_set_overwrite": "\u0627\u0644\u0643\u062a\u0627\u0628\u0629 \u0641\u0648\u0642 webhooks \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ar.json b/homeassistant/components/mqtt/translations/ar.json new file mode 100644 index 00000000000..ffdddc147df --- /dev/null +++ b/homeassistant/components/mqtt/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "broker": { + "description": "\u0627\u0644\u0631\u062c\u0627\u0621 \u0625\u062f\u062e\u0627\u0644 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0644\u0648\u0633\u064a\u0637 MQTT \u0627\u0644\u062e\u0627\u0635 \u0628\u0643." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ar.json b/homeassistant/components/nest/translations/ar.json new file mode 100644 index 00000000000..31e904a6c54 --- /dev/null +++ b/homeassistant/components/nest/translations/ar.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "unknown": "\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u0645\u0632\u0648\u062f" + }, + "title": "\u0645\u0632\u0648\u062f \u0627\u0644\u0645\u0635\u0627\u062f\u0642\u0629" + }, + "link": { + "data": { + "code": "\u0631\u0645\u0632 PIN" + }, + "title": "\u0631\u0628\u0637 \u062d\u0633\u0627\u0628 Nest" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ar.json b/homeassistant/components/nmap_tracker/translations/ar.json new file mode 100644 index 00000000000..abe5450c018 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/ar.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "\u0641\u062a\u0631\u0629 \u062a\u0643\u0631\u0627\u0631 \u0627\u0644\u0628\u062d\u062b", + "track_new_devices": "\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u062c\u062f\u064a\u062f\u0629" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ca.json b/homeassistant/components/nmap_tracker/translations/ca.json index 0912179fafd..857772081d8 100644 --- a/homeassistant/components/nmap_tracker/translations/ca.json +++ b/homeassistant/components/nmap_tracker/translations/ca.json @@ -28,7 +28,9 @@ "exclude": "Adreces de xarxa a excloure de l'escaneig (separades per comes)", "home_interval": "Nombre m\u00ednim de minuts entre escanejos de dispositius actius (conserva la bateria)", "hosts": "Adreces de xarxa a escanejar (separades per comes)", - "scan_options": "Opcions de configuraci\u00f3 d'escaneig d'Nmap en brut" + "interval_seconds": "Interval d'escaneig", + "scan_options": "Opcions de configuraci\u00f3 d'escaneig d'Nmap en brut", + "track_new_devices": "Segueix dispositius nous" }, "description": "Configura els amfitrions a explorar per Nmap. L'adre\u00e7a de xarxa i les exclusions poden ser adreces IP (192.168.1.1), xarxes IP (192.168.0.0/24) o intervals IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/et.json b/homeassistant/components/nmap_tracker/translations/et.json index 8b98dbc87cc..09b46a15889 100644 --- a/homeassistant/components/nmap_tracker/translations/et.json +++ b/homeassistant/components/nmap_tracker/translations/et.json @@ -28,7 +28,9 @@ "exclude": "V\u00e4listatud IP aadresside vahemik (komadega eraldatud list)", "home_interval": "Minimaalne sk\u00e4nnimiste intervall minutites (eeldus on aku s\u00e4\u00e4stmine)", "hosts": "V\u00f5rguaadresside vahemik (komaga eraldatud)", - "scan_options": "Vaikimisi Nmap sk\u00e4nnimise valikud" + "interval_seconds": "P\u00e4ringute intervall", + "scan_options": "Vaikimisi Nmap sk\u00e4nnimise valikud", + "track_new_devices": "Uute seadmete j\u00e4lgimine" }, "description": "Vali Nmap poolt sk\u00e4nnitavad hostid. Valikuks on IP aadressid (192.168.1.1), v\u00f5rgud (192.168.0.0/24) v\u00f5i IP vahemikud (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/ru.json b/homeassistant/components/nmap_tracker/translations/ru.json index dc488f64ca6..1a790358c73 100644 --- a/homeassistant/components/nmap_tracker/translations/ru.json +++ b/homeassistant/components/nmap_tracker/translations/ru.json @@ -28,7 +28,9 @@ "exclude": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", "home_interval": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043c\u0438\u043d\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u044f \u0437\u0430\u0440\u044f\u0434\u0430 \u0431\u0430\u0442\u0430\u0440\u0435\u0438)", "hosts": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", - "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap" + "interval_seconds": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap", + "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0445\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Nmap. \u0421\u0435\u0442\u0435\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 (192.168.1.1), IP-\u0441\u0435\u0442\u0438 (192.168.0.0/24) \u0438\u043b\u0438 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hant.json b/homeassistant/components/nmap_tracker/translations/zh-Hant.json index ce06358efc7..a2c396fdec0 100644 --- a/homeassistant/components/nmap_tracker/translations/zh-Hant.json +++ b/homeassistant/components/nmap_tracker/translations/zh-Hant.json @@ -28,7 +28,9 @@ "exclude": "\u6392\u9664\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", "home_interval": "\u6383\u63cf\u6d3b\u52d5\u88dd\u7f6e\u7684\u6700\u4f4e\u9593\u9694\u5206\u9418\uff08\u8003\u91cf\u7701\u96fb\uff09", "hosts": "\u6240\u8981\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", - "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805" + "interval_seconds": "\u6383\u63cf\u9593\u8ddd", + "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805", + "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e" }, "description": "\u8a2d\u5b9a Nmap \u6383\u63cf\u4e3b\u6a5f\u3002\u7db2\u8def\u4f4d\u5740\u8207\u6392\u9664\u4f4d\u5740\u53ef\u4ee5\u662f IP \u4f4d\u5740\uff08192.168.1.1\uff09\u3001IP \u7db2\u8def\uff08192.168.0.0/24\uff09\u6216 IP \u7bc4\u570d\uff08192.168.1.0-32\uff09\u3002" } diff --git a/homeassistant/components/onvif/translations/ar.json b/homeassistant/components/onvif/translations/ar.json new file mode 100644 index 00000000000..fb1023da658 --- /dev/null +++ b/homeassistant/components/onvif/translations/ar.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "configure": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641", + "name": "\u0627\u0644\u0627\u0633\u0645", + "password": "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631", + "port": "\u0627\u0644\u0645\u0646\u0641\u0630", + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645" + }, + "title": "\u062a\u0643\u0648\u064a\u0646 \u062c\u0647\u0627\u0632 ONVIF" + }, + "user": { + "data": { + "auto": "\u0628\u062d\u062b \u062a\u0644\u0642\u0627\u0626\u064a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ar.json b/homeassistant/components/risco/translations/ar.json new file mode 100644 index 00000000000..cb40e35cd5d --- /dev/null +++ b/homeassistant/components/risco/translations/ar.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "ha_to_risco": { + "description": "\u062d\u062f\u062f \u0627\u0644\u062d\u0627\u0644\u0629 \u0627\u0644\u062a\u064a \u062a\u0631\u064a\u062f \u0636\u0628\u0637 \u0645\u0646\u0628\u0647 Risco \u0639\u0644\u064a\u0647\u0627 \u0639\u0646\u062f \u062a\u0641\u0639\u064a\u0644 \u0625\u0646\u0630\u0627\u0631 Home Assistant" + }, + "risco_to_ha": { + "description": "\u062d\u062f\u062f \u0627\u0644\u062d\u0627\u0644\u0629 \u0627\u0644\u062a\u064a \u0633\u064a\u0628\u0644\u063a \u0639\u0646\u0647\u0627 \u0625\u0646\u0630\u0627\u0631 Home Assistant \u0644\u0643\u0644 \u062d\u0627\u0644\u0629 \u0623\u0628\u0644\u063a\u062a \u0639\u0646\u0647\u0627 Risco" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ar.json b/homeassistant/components/shelly/translations/ar.json new file mode 100644 index 00000000000..ddc96dfc9bf --- /dev/null +++ b/homeassistant/components/shelly/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm_discovery": { + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f {model} \u0639\u0644\u0649 {host} \u061f \n\n \u064a\u062c\u0628 \u0625\u064a\u0642\u0627\u0638 \u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u062a\u064a \u062a\u0639\u0645\u0644 \u0628\u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629 \u0648\u0627\u0644\u0645\u062d\u0645\u064a\u0629 \u0628\u0643\u0644\u0645\u0629 \u0645\u0631\u0648\u0631 \u0642\u0628\u0644 \u0645\u062a\u0627\u0628\u0639\u0629 \u0627\u0644\u0625\u0639\u062f\u0627\u062f.\n \u0633\u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u062a\u064a \u062a\u0639\u0645\u0644 \u0628\u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629 \u063a\u064a\u0631 \u0627\u0644\u0645\u062d\u0645\u064a\u0629 \u0628\u0643\u0644\u0645\u0629 \u0645\u0631\u0648\u0631 \u0639\u0646\u062f \u062a\u0646\u0634\u064a\u0637 \u0627\u0644\u062c\u0647\u0627\u0632 \u060c \u0648\u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0622\u0646 \u062a\u0646\u0628\u064a\u0647 \u0627\u0644\u062c\u0647\u0627\u0632 \u064a\u062f\u0648\u064a\u064b\u0627 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0632\u0631 \u0639\u0644\u064a\u0647 \u0623\u0648 \u0627\u0646\u062a\u0638\u0627\u0631 \u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u062a\u0627\u0644\u064a \u0645\u0646 \u0627\u0644\u062c\u0647\u0627\u0632." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/ar.json b/homeassistant/components/sia/translations/ar.json new file mode 100644 index 00000000000..ebf7325b114 --- /dev/null +++ b/homeassistant/components/sia/translations/ar.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "\u0625\u0646\u0634\u0627\u0621 \u0627\u062a\u0635\u0627\u0644 \u0644\u0623\u0646\u0638\u0645\u0629 \u0627\u0644\u0625\u0646\u0630\u0627\u0631 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0639\u0644\u0649 SIA." + } + } + }, + "title": "\u0623\u0646\u0638\u0645\u0629 \u0625\u0646\u0630\u0627\u0631 SIA" +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/ar.json b/homeassistant/components/sonos/translations/ar.json new file mode 100644 index 00000000000..8917893b0a1 --- /dev/null +++ b/homeassistant/components/sonos/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f Sonos\u061f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ar.json b/homeassistant/components/toon/translations/ar.json new file mode 100644 index 00000000000..be5ae6bed73 --- /dev/null +++ b/homeassistant/components/toon/translations/ar.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u062a\u0645 \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0627\u062a\u0641\u0627\u0642\u064a\u0629 \u0627\u0644\u0645\u062d\u062f\u062f\u0629 \u0628\u0627\u0644\u0641\u0639\u0644.", + "no_agreements": "\u0644\u0627 \u064a\u062d\u062a\u0648\u064a \u0647\u0630\u0627 \u0627\u0644\u062d\u0633\u0627\u0628 \u0639\u0644\u0649 \u0639\u0631\u0648\u0636 Toon." + }, + "step": { + "agreement": { + "data": { + "agreement": "\u0627\u062a\u0641\u0627\u0642\u064a\u0629" + }, + "description": "\u062d\u062f\u062f \u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0627\u062a\u0641\u0627\u0642\u064a\u0629 \u0627\u0644\u0630\u064a \u062a\u0631\u064a\u062f \u0625\u0636\u0627\u0641\u062a\u0647.", + "title": "\u062d\u062f\u062f \u0627\u062a\u0641\u0627\u0642\u064a\u062a\u0643" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.ar.json b/homeassistant/components/wolflink/translations/sensor.ar.json new file mode 100644 index 00000000000..dd850446012 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "wolflink__state": { + "bereit_keine_ladung": "\u062c\u0627\u0647\u0632 \u060c \u0644\u0627 \u064a\u062a\u0645 \u0627\u0644\u062a\u062d\u0645\u064a\u0644" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ar.json b/homeassistant/components/zha/translations/ar.json new file mode 100644 index 00000000000..e62253baf49 --- /dev/null +++ b/homeassistant/components/zha/translations/ar.json @@ -0,0 +1,7 @@ +{ + "config_panel": { + "zha_alarm_options": { + "title": "\u062e\u064a\u0627\u0631\u0627\u062a \u0644\u0648\u062d\u0629 \u0627\u0644\u062a\u062d\u0643\u0645 \u0641\u064a \u0627\u0644\u0625\u0646\u0630\u0627\u0631" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/ar.json b/homeassistant/components/zone/translations/ar.json new file mode 100644 index 00000000000..7db7791b8d6 --- /dev/null +++ b/homeassistant/components/zone/translations/ar.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "\u0627\u0644\u0627\u0633\u0645 \u0645\u0648\u062c\u0648\u062f \u0628\u0627\u0644\u0641\u0639\u0644" + }, + "step": { + "init": { + "data": { + "icon": "\u0623\u064a\u0642\u0648\u0646\u0629", + "latitude": "\u062e\u0637 \u0627\u0644\u0639\u0631\u0636", + "longitude": "\u062e\u0637 \u0627\u0644\u0637\u0648\u0644", + "name": "\u0627\u0644\u0627\u0633\u0645", + "radius": "\u0646\u0635\u0641 \u0627\u0644\u0642\u0637\u0631" + }, + "title": "\u062a\u062d\u062f\u064a\u062f \u0645\u0639\u0644\u0645\u0627\u062a \u0627\u0644\u0645\u0646\u0637\u0642\u0629" + } + }, + "title": "\u0645\u0646\u0637\u0642\u0629" + } +} \ No newline at end of file From 8f186957eafefc204e1f525abf17ae5307917443 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 3 Jul 2021 22:06:07 -0400 Subject: [PATCH 066/818] Mark entities for dead zwave_js nodes as unavailable (#48017) * Don't create any devices or entities for dead zwave_js nodes * mark entities for dead nodes as unavailable * add test * watch for node status updates * update tests to handle node status changes as well --- homeassistant/components/zwave_js/entity.py | 23 ++++++++++++++++++++- tests/components/zwave_js/common.py | 1 + tests/components/zwave_js/test_lock.py | 23 +++++++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 548796911af..432bc2fa868 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import NodeStatus from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry @@ -18,6 +19,8 @@ from .helpers import get_device_id, get_unique_id LOGGER = logging.getLogger(__name__) EVENT_VALUE_UPDATED = "value updated" +EVENT_DEAD = "dead" +EVENT_ALIVE = "alive" class ZWaveBaseEntity(Entity): @@ -90,6 +93,11 @@ class ZWaveBaseEntity(Entity): self.async_on_remove( self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) ) + for status_event in (EVENT_ALIVE, EVENT_DEAD): + self.async_on_remove( + self.info.node.on(status_event, self._node_status_alive_or_dead) + ) + self.async_on_remove( async_dispatcher_connect( self.hass, @@ -135,7 +143,20 @@ class ZWaveBaseEntity(Entity): @property def available(self) -> bool: """Return entity availability.""" - return self.client.connected and bool(self.info.node.ready) + return ( + self.client.connected + and bool(self.info.node.ready) + and self.info.node.status != NodeStatus.DEAD + ) + + @callback + def _node_status_alive_or_dead(self, event_data: dict) -> None: + """ + Call when node status changes to alive or dead. + + Should not be overridden by subclasses. + """ + self.async_write_ha_state() @callback def _value_changed(self, event_data: dict) -> None: diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index de80eb6bbc5..f14842609c5 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -22,6 +22,7 @@ CLIMATE_MAIN_HEAT_ACTIONNER = "climate.main_heat_actionner" BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" EATON_RF9640_ENTITY = "light.allloaddimmer" AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6" +SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt" ID_LOCK_CONFIG_PARAMETER_SENSOR = ( "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode" ) diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 9ddc7abdd88..3727ab9d288 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -1,6 +1,7 @@ """Test the Z-Wave JS lock platform.""" from zwave_js_server.const import ATTR_CODE_SLOT, ATTR_USERCODE from zwave_js_server.event import Event +from zwave_js_server.model.node import NodeStatus from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -12,9 +13,14 @@ from homeassistant.components.zwave_js.lock import ( SERVICE_CLEAR_LOCK_USERCODE, SERVICE_SET_LOCK_USERCODE, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNLOCKED, +) -SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt" +from .common import SCHLAGE_BE469_LOCK_ENTITY async def test_door_lock(hass, client, lock_schlage_be469, integration): @@ -203,3 +209,16 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): "value": 1, } assert args["value"] == 0 + + event = Event( + type="dead", + data={ + "source": "node", + "event": "dead", + "nodeId": 20, + }, + ) + node.receive_event(event) + + assert node.status == NodeStatus.DEAD + assert hass.states.get(SCHLAGE_BE469_LOCK_ENTITY).state == STATE_UNAVAILABLE From 77a06e01f7e490f0ec9508b4cecd86a47ba45d45 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sun, 4 Jul 2021 10:56:16 +0200 Subject: [PATCH 067/818] Update devolo-home-control-api (#52497) --- homeassistant/components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 5886c1d0fe2..9621a49157a 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.17.3"], + "requirements": ["devolo-home-control-api==0.17.4"], "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], diff --git a/requirements_all.txt b/requirements_all.txt index c69a1a3daf9..e24f8bd59aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ deluge-client==1.7.1 denonavr==0.10.8 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.3 +devolo-home-control-api==0.17.4 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a0f22c4d62..3275c72d46f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -285,7 +285,7 @@ defusedxml==0.7.1 denonavr==0.10.8 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.3 +devolo-home-control-api==0.17.4 # homeassistant.components.directv directv==0.4.0 From 73c6aa701ff582ad6e7fc1fffa42d204c82a7eee Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Sun, 4 Jul 2021 13:16:27 +0200 Subject: [PATCH 068/818] Add basic typing to ezviz camera platform (#52492) * Add basic typing to camera platform. Small cleanups. * Add full typing to all functions --- homeassistant/components/ezviz/camera.py | 101 ++++++++++++++--------- homeassistant/components/ezviz/const.py | 2 +- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index b09e5cdd901..76fbaee3757 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,4 +1,6 @@ """Support ezviz camera devices.""" +from __future__ import annotations + import asyncio import logging @@ -8,10 +10,17 @@ import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT +from homeassistant.config_entries import ( + SOURCE_DISCOVERY, + SOURCE_IGNORE, + SOURCE_IMPORT, + ConfigEntry, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -39,6 +48,7 @@ from .const import ( SERVICE_PTZ, SERVICE_WAKE_DEVICE, ) +from .coordinator import EzvizDataUpdateCoordinator CAMERA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} @@ -55,7 +65,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: entity_platform.AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up a Ezviz IP Camera from platform config.""" _LOGGER.warning( "Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards" @@ -91,10 +106,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: """Set up Ezviz cameras based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] camera_config_entries = hass.config_entries.async_entries(DOMAIN) camera_entities = [] @@ -169,7 +190,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(camera_entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_PTZ, @@ -210,20 +231,22 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): +class EzvizCamera(CoordinatorEntity, Camera): """An implementation of a Ezviz security camera.""" + coordinator: EzvizDataUpdateCoordinator + def __init__( self, - hass, - coordinator, - idx, - camera_username, - camera_password, - camera_rtsp_stream, - local_rtsp_port, - ffmpeg_arguments, - ): + hass: HomeAssistant, + coordinator: EzvizDataUpdateCoordinator, + idx: int, + camera_username: str, + camera_password: str, + camera_rtsp_stream: str | None, + local_rtsp_port: int | None, + ffmpeg_arguments: str | None, + ) -> None: """Initialize a Ezviz security camera.""" super().__init__(coordinator) Camera.__init__(self) @@ -240,51 +263,48 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): self._local_ip = self.coordinator.data[self._idx]["local_ip"] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" - if self.coordinator.data[self._idx]["status"] == 2: - return False - - return True + return self.coordinator.data[self._idx]["status"] != 2 @property - def supported_features(self): + def supported_features(self) -> int: """Return supported features.""" if self._rtsp_stream: return SUPPORT_STREAM return 0 @property - def name(self): + def name(self) -> str: """Return the name of this device.""" return self._name @property - def model(self): + def model(self) -> str: """Return the model of this device.""" return self.coordinator.data[self._idx]["device_sub_category"] @property - def brand(self): + def brand(self) -> str: """Return the manufacturer of this device.""" return MANUFACTURER @property - def is_on(self): + def is_on(self) -> bool: """Return true if on.""" return bool(self.coordinator.data[self._idx]["status"]) @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" return self.coordinator.data[self._idx]["alarm_notify"] @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Camera Motion Detection Status.""" return self.coordinator.data[self._idx]["alarm_notify"] - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" try: self.coordinator.ezviz_client.set_camera_defence(self._serial, 1) @@ -292,7 +312,7 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): except InvalidHost as err: raise InvalidHost("Error enabling motion detection") from err - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection.""" try: self.coordinator.ezviz_client.set_camera_defence(self._serial, 0) @@ -301,11 +321,11 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): raise InvalidHost("Error disabling motion detection") from err @property - def unique_id(self): + def unique_id(self) -> str: """Return the name of this camera.""" return self._serial - async def async_camera_image(self): + async def async_camera_image(self) -> bytes | None: """Return a frame from the camera stream.""" ffmpeg = ImageFrame(self._ffmpeg.binary) @@ -315,7 +335,7 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): return image @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, self._serial)}, @@ -325,7 +345,7 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): "sw_version": self.coordinator.data[self._idx]["version"], } - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the stream source.""" local_ip = self.coordinator.data[self._idx]["local_ip"] if self._local_rtsp_port: @@ -340,9 +360,8 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): return rtsp_stream_source return None - def perform_ptz(self, direction, speed): + def perform_ptz(self, direction: str, speed: int) -> None: """Perform a PTZ action on the camera.""" - _LOGGER.debug("PTZ action '%s' on %s", direction, self._name) try: self.coordinator.ezviz_client.ptz_control( str(direction).upper(), self._serial, "START", speed @@ -354,21 +373,21 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): except HTTPError as err: raise HTTPError("Cannot perform PTZ") from err - def perform_sound_alarm(self, enable): + def perform_sound_alarm(self, enable: int) -> None: """Sound the alarm on a camera.""" try: self.coordinator.ezviz_client.sound_alarm(self._serial, enable) except HTTPError as err: raise HTTPError("Cannot sound alarm") from err - def perform_wake_device(self): + def perform_wake_device(self) -> None: """Basically wakes the camera by querying the device.""" try: self.coordinator.ezviz_client.get_detection_sensibility(self._serial) except (HTTPError, PyEzvizError) as err: raise PyEzvizError("Cannot wake device") from err - def perform_alarm_sound(self, level): + def perform_alarm_sound(self, level: int) -> None: """Enable/Disable movement sound alarm.""" try: self.coordinator.ezviz_client.alarm_sound(self._serial, level, 1) @@ -377,7 +396,9 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): "Cannot set alarm sound level for on movement detected" ) from err - def perform_set_alarm_detection_sensibility(self, level, type_value): + def perform_set_alarm_detection_sensibility( + self, level: int, type_value: int + ) -> None: """Set camera detection sensibility level service.""" try: self.coordinator.ezviz_client.detection_sensibility( diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index e3e2cae712c..ec1471d8bc4 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -34,7 +34,7 @@ SERVICE_DETECTION_SENSITIVITY = "set_alarm_detection_sensibility" EU_URL = "apiieu.ezvizlife.com" RUSSIA_URL = "apirus.ezvizru.com" DEFAULT_CAMERA_USERNAME = "admin" -DEFAULT_RTSP_PORT = "554" +DEFAULT_RTSP_PORT = 554 DEFAULT_TIMEOUT = 25 DEFAULT_FFMPEG_ARGUMENTS = "" From cfe2017dd97cb5baa822893de3095b267b684e35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Jul 2021 09:16:42 -0500 Subject: [PATCH 069/818] Bump HAP-python to 3.5.1 (#52508) - Fixes additional cases of invalid mdns hostnames --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index d2c2f094a0f..39c40e03614 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.5.0", + "HAP-python==3.5.1", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index e24f8bd59aa..f242c50d034 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.5.0 +HAP-python==3.5.1 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3275c72d46f..c1c13a2e45a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.5.0 +HAP-python==3.5.1 # homeassistant.components.flick_electric PyFlick==0.0.2 From 8ccb338a9b05f03766743077cd17fa1abf18fadb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 4 Jul 2021 12:33:52 -0400 Subject: [PATCH 070/818] Use entity class attributes for airnow (#52502) --- homeassistant/components/airnow/sensor.py | 34 ++++------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 2d3adc8d1e2..31ea5e298e3 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -67,16 +67,13 @@ class AirNowSensor(CoordinatorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) self.kind = kind - self._device_class = None self._state = None - self._icon = None - self._unit_of_measurement = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def name(self): - """Return the name.""" - return f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON] + self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + self._attr_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT] + self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" @property def state(self): @@ -96,24 +93,3 @@ class AirNowSensor(CoordinatorEntity, SensorEntity): ] return self._attrs - - @property - def icon(self): - """Return the icon.""" - self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] - return self._icon - - @property - def device_class(self): - """Return the device_class.""" - return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return SENSOR_TYPES[self.kind][ATTR_UNIT] From 27295d8f585dd1c7f2624b14f785256c30c598ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Jul 2021 11:40:33 -0500 Subject: [PATCH 071/818] Remove empty hosts and excludes from nmap configuration (#52489) --- homeassistant/components/nmap_tracker/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 399121e4e00..76a7e44f153 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -166,8 +166,10 @@ class NmapDeviceScanner: self._scan_interval = timedelta( seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) ) - self._hosts = cv.ensure_list_csv(config[CONF_HOSTS]) - self._exclude = cv.ensure_list_csv(config[CONF_EXCLUDE]) + hosts_list = cv.ensure_list_csv(config[CONF_HOSTS]) + self._hosts = [host for host in hosts_list if host != ""] + excludes_list = cv.ensure_list_csv(config[CONF_EXCLUDE]) + self._exclude = [exclude for exclude in excludes_list if exclude != ""] self._options = config[CONF_OPTIONS] self.home_interval = timedelta( minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) From bdf247faaab79cefdbee5f7a24cb68d07576b4cc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 4 Jul 2021 18:54:07 +0200 Subject: [PATCH 072/818] Migrate GIOS air_quality platform to sensor (#52295) --- homeassistant/components/gios/__init__.py | 13 +- homeassistant/components/gios/air_quality.py | 157 --------------- homeassistant/components/gios/const.py | 90 +++++---- homeassistant/components/gios/model.py | 12 ++ homeassistant/components/gios/sensor.py | 89 +++++++++ tests/components/gios/test_air_quality.py | 145 -------------- tests/components/gios/test_init.py | 22 ++- tests/components/gios/test_sensor.py | 190 +++++++++++++++++++ 8 files changed, 378 insertions(+), 340 deletions(-) delete mode 100644 homeassistant/components/gios/air_quality.py create mode 100644 homeassistant/components/gios/model.py create mode 100644 homeassistant/components/gios/sensor.py delete mode 100644 tests/components/gios/test_air_quality.py create mode 100644 tests/components/gios/test_sensor.py diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index ab956fe9da7..c3227254075 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -9,8 +9,10 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from gios import ApiError, Gios, InvalidSensorsData, NoStationError +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,7 +21,7 @@ from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["air_quality"] +PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -49,6 +51,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Remove air_quality entities from registry if they exist + ent_reg = entity_registry.async_get(hass) + unique_id = str(coordinator.gios.station_id) + if entity_id := ent_reg.async_get_entity_id( + AIR_QUALITY_PLATFORM, DOMAIN, unique_id + ): + _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id) + ent_reg.async_remove(entity_id) + return True diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py deleted file mode 100644 index e74cec8e151..00000000000 --- a/homeassistant/components/gios/air_quality.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Support for the GIOS service.""" -from __future__ import annotations - -from typing import Any, Optional, cast - -from homeassistant.components.air_quality import AirQualityEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import GiosDataUpdateCoordinator -from .const import ( - API_AQI, - API_CO, - API_NO2, - API_O3, - API_PM10, - API_PM25, - API_SO2, - ATTR_STATION, - ATTRIBUTION, - DEFAULT_NAME, - DOMAIN, - ICONS_MAP, - MANUFACTURER, - SENSOR_MAP, -) - -PARALLEL_UPDATES = 1 - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Add a GIOS entities from a config_entry.""" - name = entry.data[CONF_NAME] - - coordinator = hass.data[DOMAIN][entry.entry_id] - - # We used to use int as entity unique_id, convert this to str. - entity_registry = await async_get_registry(hass) - old_entity_id = entity_registry.async_get_entity_id( - "air_quality", DOMAIN, coordinator.gios.station_id - ) - if old_entity_id is not None: - entity_registry.async_update_entity( - old_entity_id, new_unique_id=str(coordinator.gios.station_id) - ) - - async_add_entities([GiosAirQuality(coordinator, name)]) - - -class GiosAirQuality(CoordinatorEntity, AirQualityEntity): - """Define an GIOS sensor.""" - - coordinator: GiosDataUpdateCoordinator - - def __init__(self, coordinator: GiosDataUpdateCoordinator, name: str) -> None: - """Initialize.""" - super().__init__(coordinator) - self._name = name - self._attrs: dict[str, Any] = {} - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon.""" - if self.air_quality_index is not None and self.air_quality_index in ICONS_MAP: - return ICONS_MAP[self.air_quality_index] - return "mdi:blur" - - @property - def air_quality_index(self) -> str | None: - """Return the air quality index.""" - return cast(Optional[str], self.coordinator.data.get(API_AQI, {}).get("value")) - - @property - def particulate_matter_2_5(self) -> float | None: - """Return the particulate matter 2.5 level.""" - return round_state(self._get_sensor_value(API_PM25)) - - @property - def particulate_matter_10(self) -> float | None: - """Return the particulate matter 10 level.""" - return round_state(self._get_sensor_value(API_PM10)) - - @property - def ozone(self) -> float | None: - """Return the O3 (ozone) level.""" - return round_state(self._get_sensor_value(API_O3)) - - @property - def carbon_monoxide(self) -> float | None: - """Return the CO (carbon monoxide) level.""" - return round_state(self._get_sensor_value(API_CO)) - - @property - def sulphur_dioxide(self) -> float | None: - """Return the SO2 (sulphur dioxide) level.""" - return round_state(self._get_sensor_value(API_SO2)) - - @property - def nitrogen_dioxide(self) -> float | None: - """Return the NO2 (nitrogen dioxide) level.""" - return round_state(self._get_sensor_value(API_NO2)) - - @property - def attribution(self) -> str: - """Return the attribution.""" - return ATTRIBUTION - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return str(self.coordinator.gios.station_id) - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return { - "identifiers": {(DOMAIN, str(self.coordinator.gios.station_id))}, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes.""" - # Different measuring stations have different sets of sensors. We don't know - # what data we will get. - for sensor in SENSOR_MAP: - if sensor in self.coordinator.data: - self._attrs[f"{SENSOR_MAP[sensor]}_index"] = self.coordinator.data[ - sensor - ]["index"] - self._attrs[ATTR_STATION] = self.coordinator.gios.station_name - return self._attrs - - def _get_sensor_value(self, sensor: str) -> float | None: - """Return value of specified sensor.""" - if sensor in self.coordinator.data: - return cast(float, self.coordinator.data[sensor]["value"]) - return None - - -def round_state(state: float | None) -> float | None: - """Round state.""" - return round(state) if state is not None else None diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index d16225d90a7..f78e876a0e7 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,18 +4,13 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.air_quality import ( - ATTR_CO, - ATTR_NO2, - ATTR_OZONE, - ATTR_PM_2_5, - ATTR_PM_10, - ATTR_SO2, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + +from .model import SensorDescription ATTRIBUTION: Final = "Data provided by GIOŚ" -ATTR_STATION: Final = "station" CONF_STATION_ID: Final = "station_id" DEFAULT_NAME: Final = "GIOŚ" # Term of service GIOŚ allow downloading data no more than twice an hour. @@ -23,35 +18,58 @@ SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" MANUFACTURER: Final = "Główny Inspektorat Ochrony Środowiska" -API_AQI: Final = "aqi" -API_CO: Final = "co" -API_NO2: Final = "no2" -API_O3: Final = "o3" -API_PM10: Final = "pm10" -API_PM25: Final = "pm2.5" -API_SO2: Final = "so2" - API_TIMEOUT: Final = 30 -AQI_GOOD: Final = "dobry" -AQI_MODERATE: Final = "umiarkowany" -AQI_POOR: Final = "dostateczny" -AQI_VERY_GOOD: Final = "bardzo dobry" -AQI_VERY_POOR: Final = "zły" +ATTR_INDEX: Final = "index" +ATTR_STATION: Final = "station" +ATTR_UNIT: Final = "unit" +ATTR_VALUE: Final = "value" +ATTR_STATION_NAME: Final = "station_name" -ICONS_MAP: Final[dict[str, str]] = { - AQI_VERY_GOOD: "mdi:emoticon-excited", - AQI_GOOD: "mdi:emoticon-happy", - AQI_MODERATE: "mdi:emoticon-neutral", - AQI_POOR: "mdi:emoticon-sad", - AQI_VERY_POOR: "mdi:emoticon-dead", -} +ATTR_C6H6: Final = "c6h6" +ATTR_CO: Final = "co" +ATTR_NO2: Final = "no2" +ATTR_O3: Final = "o3" +ATTR_PM10: Final = "pm10" +ATTR_PM25: Final = "pm2.5" +ATTR_SO2: Final = "so2" +ATTR_AQI: Final = "aqi" -SENSOR_MAP: Final[dict[str, str]] = { - API_CO: ATTR_CO, - API_NO2: ATTR_NO2, - API_O3: ATTR_OZONE, - API_PM10: ATTR_PM_10, - API_PM25: ATTR_PM_2_5, - API_SO2: ATTR_SO2, +SENSOR_TYPES: Final[dict[str, SensorDescription]] = { + ATTR_AQI: {}, + ATTR_C6H6: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_CO: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_NO2: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_O3: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_PM10: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_PM25: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_SO2: { + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, } diff --git a/homeassistant/components/gios/model.py b/homeassistant/components/gios/model.py new file mode 100644 index 00000000000..867c950e183 --- /dev/null +++ b/homeassistant/components/gios/model.py @@ -0,0 +1,12 @@ +"""Type definitions for GIOS integration.""" +from __future__ import annotations + +from typing import Callable, TypedDict + + +class SensorDescription(TypedDict, total=False): + """Sensor description class.""" + + unit: str + state_class: str + value: Callable diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py new file mode 100644 index 00000000000..c0bda830f25 --- /dev/null +++ b/homeassistant/components/gios/sensor.py @@ -0,0 +1,89 @@ +"""Support for the GIOS service.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import GiosDataUpdateCoordinator +from .const import ( + ATTR_INDEX, + ATTR_STATION, + ATTR_UNIT, + ATTR_VALUE, + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + MANUFACTURER, + SENSOR_TYPES, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add a GIOS entities from a config_entry.""" + name = entry.data[CONF_NAME] + + coordinator = hass.data[DOMAIN][entry.entry_id] + + sensors = [] + + for sensor in coordinator.data: + if sensor in SENSOR_TYPES: + sensors.append(GiosSensor(name, sensor, coordinator)) + async_add_entities(sensors) + + +class GiosSensor(CoordinatorEntity, SensorEntity): + """Define an GIOS sensor.""" + + coordinator: GiosDataUpdateCoordinator + + def __init__( + self, name: str, sensor_type: str, coordinator: GiosDataUpdateCoordinator + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._description = SENSOR_TYPES[sensor_type] + self._attr_device_info = { + "identifiers": {(DOMAIN, str(coordinator.gios.station_id))}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + self._attr_icon = "mdi:blur" + self._attr_name = f"{name} {sensor_type.upper()}" + self._attr_state_class = self._description.get(ATTR_STATE_CLASS) + self._attr_unique_id = f"{coordinator.gios.station_id}-{sensor_type}" + self._attr_unit_of_measurement = self._description.get(ATTR_UNIT) + self._sensor_type = sensor_type + self._state = None + self._attrs: dict[str, Any] = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_STATION: self.coordinator.gios.station_name, + } + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + if self.coordinator.data[self._sensor_type].get(ATTR_INDEX): + self._attrs[ATTR_NAME] = self.coordinator.data[self._sensor_type][ATTR_NAME] + self._attrs[ATTR_INDEX] = self.coordinator.data[self._sensor_type][ + ATTR_INDEX + ] + return self._attrs + + @property + def state(self) -> StateType: + """Return the state.""" + self._state = self.coordinator.data[self._sensor_type][ATTR_VALUE] + if self._description.get(ATTR_VALUE): + return cast(StateType, self._description[ATTR_VALUE](self._state)) + return cast(StateType, self._state) diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py deleted file mode 100644 index b7ce8d1f97a..00000000000 --- a/tests/components/gios/test_air_quality.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Test air_quality of GIOS integration.""" -from datetime import timedelta -import json -from unittest.mock import patch - -from gios import ApiError - -from homeassistant.components.air_quality import ( - ATTR_AQI, - ATTR_CO, - ATTR_NO2, - ATTR_OZONE, - ATTR_PM_2_5, - ATTR_PM_10, - ATTR_SO2, - DOMAIN as AIR_QUALITY_DOMAIN, -) -from homeassistant.components.gios.air_quality import ATTRIBUTION -from homeassistant.components.gios.const import AQI_GOOD, DOMAIN -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - STATE_UNAVAILABLE, -) -from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow - -from tests.common import async_fire_time_changed, load_fixture -from tests.components.gios import init_integration - - -async def test_air_quality(hass): - """Test states of the air_quality.""" - await init_integration(hass) - registry = er.async_get(hass) - - state = hass.states.get("air_quality.home") - assert state - assert state.state == "4" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_AQI) == AQI_GOOD - assert state.attributes.get(ATTR_PM_10) == 17 - assert state.attributes.get(ATTR_PM_2_5) == 4 - assert state.attributes.get(ATTR_CO) == 252 - assert state.attributes.get(ATTR_SO2) == 4 - assert state.attributes.get(ATTR_NO2) == 7 - assert state.attributes.get(ATTR_OZONE) == 96 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) == "mdi:emoticon-happy" - assert state.attributes.get("station") == "Test Name 1" - - entry = registry.async_get("air_quality.home") - assert entry - assert entry.unique_id == "123" - - -async def test_air_quality_with_incomplete_data(hass): - """Test states of the air_quality with incomplete data from measuring station.""" - await init_integration(hass, incomplete_data=True) - registry = er.async_get(hass) - - state = hass.states.get("air_quality.home") - assert state - assert state.state == "4" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_AQI) == "foo" - assert state.attributes.get(ATTR_PM_10) is None - assert state.attributes.get(ATTR_PM_2_5) == 4 - assert state.attributes.get(ATTR_CO) == 252 - assert state.attributes.get(ATTR_SO2) == 4 - assert state.attributes.get(ATTR_NO2) == 7 - assert state.attributes.get(ATTR_OZONE) == 96 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" - assert state.attributes.get("station") == "Test Name 1" - - entry = registry.async_get("air_quality.home") - assert entry - assert entry.unique_id == "123" - - -async def test_availability(hass): - """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass) - - state = hass.states.get("air_quality.home") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "4" - - future = utcnow() + timedelta(minutes=60) - with patch( - "homeassistant.components.gios.Gios._get_all_sensors", - side_effect=ApiError("Unexpected error"), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.home") - assert state - assert state.state == STATE_UNAVAILABLE - - future = utcnow() + timedelta(minutes=120) - with patch( - "homeassistant.components.gios.Gios._get_all_sensors", - return_value=json.loads(load_fixture("gios/sensors.json")), - ), patch( - "homeassistant.components.gios.Gios._get_indexes", - return_value=json.loads(load_fixture("gios/indexes.json")), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.home") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "4" - - -async def test_migrate_unique_id(hass): - """Test migrate unique_id of the air_quality entity.""" - registry = er.async_get(hass) - - # Pre-create registry entries for disabled by default sensors - registry.async_get_or_create( - AIR_QUALITY_DOMAIN, - DOMAIN, - 123, - suggested_object_id="home", - disabled_by=None, - ) - - await init_integration(hass) - - entry = registry.async_get("air_quality.home") - assert entry - assert entry.unique_id == "123" diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 08629608cd4..f0b3f660e8d 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -2,9 +2,11 @@ import json from unittest.mock import patch +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.gios.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er from . import STATIONS @@ -16,7 +18,7 @@ async def test_async_setup_entry(hass): """Test a successful setup entry.""" await init_integration(hass) - state = hass.states.get("air_quality.home") + state = hass.states.get("sensor.home_pm2_5") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == "4" @@ -95,3 +97,21 @@ async def test_migrate_device_and_config_entry(hass): config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123")} ) assert device_entry.id == migrated_device_entry.id + + +async def test_remove_air_quality_entities(hass): + """Test remove air_quality entities from registry.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + AIR_QUALITY_PLATFORM, + DOMAIN, + "123", + suggested_object_id="home", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("air_quality.home") + assert entry is None diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py new file mode 100644 index 00000000000..4d43d90f9a2 --- /dev/null +++ b/tests/components/gios/test_sensor.py @@ -0,0 +1,190 @@ +"""Test sensor of GIOS integration.""" +from datetime import timedelta +import json +from unittest.mock import patch + +from gios import ApiError + +from homeassistant.components.gios.const import ATTR_STATION, ATTRIBUTION +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + STATE_UNAVAILABLE, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed, load_fixture +from tests.components.gios import init_integration + + +async def test_sensor(hass): + """Test states of the sensor.""" + await init_integration(hass) + registry = er.async_get(hass) + + state = hass.states.get("sensor.home_c6h6") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_c6h6") + assert entry + assert entry.unique_id == "123-c6h6" + + state = hass.states.get("sensor.home_co") + assert state + assert state.state == "252" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_co") + assert entry + assert entry.unique_id == "123-co" + + state = hass.states.get("sensor.home_no2") + assert state + assert state.state == "7" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_no2") + assert entry + assert entry.unique_id == "123-no2" + + state = hass.states.get("sensor.home_o3") + assert state + assert state.state == "96" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_o3") + assert entry + assert entry.unique_id == "123-o3" + + state = hass.states.get("sensor.home_pm10") + assert state + assert state.state == "17" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_pm10") + assert entry + assert entry.unique_id == "123-pm10" + + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == "4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_pm2_5") + assert entry + assert entry.unique_id == "123-pm2.5" + + state = hass.states.get("sensor.home_so2") + assert state + assert state.state == "4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_so2") + assert entry + assert entry.unique_id == "123-so2" + + state = hass.states.get("sensor.home_aqi") + assert state + assert state.state == "dobry" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_aqi") + assert entry + assert entry.unique_id == "123-aqi" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "4" + + future = utcnow() + timedelta(minutes=60) + with patch( + "homeassistant.components.gios.Gios._get_all_sensors", + side_effect=ApiError("Unexpected error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=120) + with patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=json.loads(load_fixture("gios/sensors.json")), + ), patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value=json.loads(load_fixture("gios/indexes.json")), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "4" From 5ce4de7bbbc4d9cb7d54aa984541fcdaf3268a0c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 4 Jul 2021 13:16:06 -0500 Subject: [PATCH 073/818] Migrate AirVisual `air_quality` platform to `sensor` platform (#52349) * Migrate AirVisual air_quality platform sensors * Remove redundancy * Cleanup * Properly set available * Unwind config_entry -> entry name change * More unwinding * Rename * Update homeassistant/components/airvisual/sensor.py Co-authored-by: Franck Nijhof * Code review * Linting Co-authored-by: Franck Nijhof --- .coveragerc | 1 - .../components/airvisual/__init__.py | 36 ++-- .../components/airvisual/air_quality.py | 108 ------------ homeassistant/components/airvisual/sensor.py | 163 +++++++++++------- 4 files changed, 123 insertions(+), 185 deletions(-) delete mode 100644 homeassistant/components/airvisual/air_quality.py diff --git a/.coveragerc b/.coveragerc index 6bf9936e26b..74797f83087 100644 --- a/.coveragerc +++ b/.coveragerc @@ -35,7 +35,6 @@ omit = homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py homeassistant/components/airvisual/__init__.py - homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/* homeassistant/components/alarmdecoder/__init__.py diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 8a1e0ad9655..89963bff623 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -22,7 +22,11 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + entity_registry, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -42,7 +46,7 @@ from .const import ( LOGGER, ) -PLATFORMS = ["air_quality", "sensor"] +PLATFORMS = ["sensor"] DATA_LISTENER = "listener" @@ -124,12 +128,6 @@ def async_sync_geo_coordinator_update_intervals(hass, api_key): coordinator.update_interval = update_interval -async def async_setup(hass, config): - """Set up the AirVisual component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}} - return True - - @callback def _standardize_geography_config_entry(hass, config_entry): """Ensure that geography config entries have appropriate properties.""" @@ -183,6 +181,8 @@ def _standardize_node_pro_config_entry(hass, config_entry): async def async_setup_entry(hass, config_entry): """Set up AirVisual as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) + if CONF_API_KEY in config_entry.data: _standardize_geography_config_entry(hass, config_entry) @@ -227,6 +227,19 @@ async def async_setup_entry(hass, config_entry): config_entry.entry_id ] = config_entry.add_update_listener(async_reload_entry) else: + # Remove outdated air_quality entities from the entity registry if they exist: + ent_reg = entity_registry.async_get(hass) + for entity_entry in [ + e + for e in ent_reg.entities.values() + if e.config_entry_id == config_entry.entry_id + and e.entity_id.startswith("air_quality") + ]: + LOGGER.debug( + 'Removing deprecated air_quality entity: "%s"', entity_entry.entity_id + ) + ent_reg.async_remove(entity_entry.entity_id) + _standardize_node_pro_config_entry(hass, config_entry) async def async_update_data(): @@ -336,12 +349,7 @@ class AirVisualEntity(CoordinatorEntity): def __init__(self, coordinator): """Initialize.""" super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._attrs + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py deleted file mode 100644 index 175c129068f..00000000000 --- a/homeassistant/components/airvisual/air_quality.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Support for AirVisual Node/Pro units.""" -from homeassistant.components.air_quality import AirQualityEntity -from homeassistant.core import callback - -from . import AirVisualEntity -from .const import ( - CONF_INTEGRATION_TYPE, - DATA_COORDINATOR, - DOMAIN, - INTEGRATION_TYPE_NODE_PRO, -) - -ATTR_HUMIDITY = "humidity" -ATTR_SENSOR_LIFE = "{0}_sensor_life" -ATTR_VOC = "voc" - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up AirVisual air quality entities based on a config entry.""" - # Geography-based AirVisual integrations don't utilize this platform: - if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO: - return - - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] - - async_add_entities([AirVisualNodeProSensor(coordinator)], True) - - -class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): - """Define a sensor for a AirVisual Node/Pro.""" - - def __init__(self, airvisual): - """Initialize.""" - super().__init__(airvisual) - - self._attr_icon = "mdi:chemical-weapon" - - @property - def air_quality_index(self): - """Return the Air Quality Index (AQI).""" - if self.coordinator.data["settings"]["is_aqi_usa"]: - return self.coordinator.data["measurements"]["aqi_us"] - return self.coordinator.data["measurements"]["aqi_cn"] - - @property - def available(self): - """Return True if entity is available.""" - return bool(self.coordinator.data) - - @property - def carbon_dioxide(self): - """Return the CO2 (carbon dioxide) level.""" - return self.coordinator.data["measurements"].get("co2") - - @property - def device_info(self): - """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, - "name": self.coordinator.data["settings"]["node_name"], - "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["status"]["model"]}', - "sw_version": ( - f'Version {self.coordinator.data["status"]["system_version"]}' - f'{self.coordinator.data["status"]["app_version"]}' - ), - } - - @property - def name(self): - """Return the name.""" - node_name = self.coordinator.data["settings"]["node_name"] - return f"{node_name} Node/Pro: Air Quality" - - @property - def particulate_matter_2_5(self): - """Return the particulate matter 2.5 level.""" - return self.coordinator.data["measurements"].get("pm2_5") - - @property - def particulate_matter_10(self): - """Return the particulate matter 10 level.""" - return self.coordinator.data["measurements"].get("pm1_0") - - @property - def particulate_matter_0_1(self): - """Return the particulate matter 0.1 level.""" - return self.coordinator.data["measurements"].get("pm0_1") - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self.coordinator.data["serial_number"] - - @callback - def update_from_latest_data(self): - """Update the entity from the latest data.""" - self._attrs.update( - { - ATTR_VOC: self.coordinator.data["measurements"].get("voc"), - **{ - ATTR_SENSOR_LIFE.format(pollutant): lifespan - for pollutant, lifespan in self.coordinator.data["status"][ - "sensor_life" - ].items() - }, - } - ) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index c5d6621a329..fb6cc0c5e26 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, CONF_STATE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -36,12 +37,18 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol" ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_REGION = "region" -SENSOR_KIND_LEVEL = "air_pollution_level" SENSOR_KIND_AQI = "air_quality_index" -SENSOR_KIND_POLLUTANT = "main_pollutant" SENSOR_KIND_BATTERY_LEVEL = "battery_level" +SENSOR_KIND_CO2 = "carbon_dioxide" SENSOR_KIND_HUMIDITY = "humidity" +SENSOR_KIND_LEVEL = "air_pollution_level" +SENSOR_KIND_PM_0_1 = "particulate_matter_0_1" +SENSOR_KIND_PM_1_0 = "particulate_matter_1_0" +SENSOR_KIND_PM_2_5 = "particulate_matter_2_5" +SENSOR_KIND_POLLUTANT = "main_pollutant" +SENSOR_KIND_SENSOR_LIFE = "sensor_life" SENSOR_KIND_TEMPERATURE = "temperature" +SENSOR_KIND_VOC = "voc" GEOGRAPHY_SENSORS = [ (SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None), @@ -51,9 +58,51 @@ GEOGRAPHY_SENSORS = [ GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} NODE_PRO_SENSORS = [ - (SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, PERCENTAGE), - (SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE), - (SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), + (SENSOR_KIND_AQI, "Air Quality Index", None, "mdi:chart-line", "AQI"), + (SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, None, PERCENTAGE), + ( + SENSOR_KIND_CO2, + "C02", + DEVICE_CLASS_CO2, + None, + CONCENTRATION_PARTS_PER_MILLION, + ), + (SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, None, PERCENTAGE), + ( + SENSOR_KIND_PM_0_1, + "PM 0.1", + None, + "mdi:sprinkler", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + ( + SENSOR_KIND_PM_1_0, + "PM 1.0", + None, + "mdi:sprinkler", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + ( + SENSOR_KIND_PM_2_5, + "PM 2.5", + None, + "mdi:sprinkler", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + ( + SENSOR_KIND_TEMPERATURE, + "Temperature", + DEVICE_CLASS_TEMPERATURE, + None, + TEMP_CELSIUS, + ), + ( + SENSOR_KIND_VOC, + "VOC", + None, + "mdi:sprinkler", + CONCENTRATION_PARTS_PER_MILLION, + ), ] POLLUTANT_LABELS = { @@ -107,8 +156,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] else: sensors = [ - AirVisualNodeProSensor(coordinator, kind, name, device_class, unit) - for kind, name, device_class, unit in NODE_PRO_SENSORS + AirVisualNodeProSensor(coordinator, kind, name, device_class, icon, unit) + for kind, name, device_class, icon, unit in NODE_PRO_SENSORS ] async_add_entities(sensors, True) @@ -121,46 +170,25 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_CITY: config_entry.data.get(CONF_CITY), ATTR_STATE: config_entry.data.get(CONF_STATE), ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY), } ) + self._attr_icon = icon + self._attr_name = f"{GEOGRAPHY_SENSOR_LOCALES[locale]} {name}" + self._attr_unique_id = f"{config_entry.unique_id}_{locale}_{kind}" + self._attr_unit_of_measurement = unit self._config_entry = config_entry self._kind = kind self._locale = locale - self._name = name - self._state = None - - self._attr_icon = icon - self._attr_unit_of_measurement = unit @property - def available(self): - """Return True if entity is available.""" - try: - return self.coordinator.last_update_success and bool( - self.coordinator.data["current"]["pollution"] - ) - except KeyError: - return False - - @property - def name(self): - """Return the name.""" - return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}" - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._config_entry.unique_id}_{self._locale}_{self._kind}" + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.coordinator.data["current"]["pollution"] @callback def update_from_latest_data(self): @@ -172,17 +200,17 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): if self._kind == SENSOR_KIND_LEVEL: aqi = data[f"aqi{self._locale}"] - [(self._state, self._attr_icon)] = [ + [(self._attr_state, self._attr_icon)] = [ (name, icon) for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items() if floor <= aqi <= ceiling ] elif self._kind == SENSOR_KIND_AQI: - self._state = data[f"aqi{self._locale}"] + self._attr_state = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: symbol = data[f"main{self._locale}"] - self._state = POLLUTANT_LABELS[symbol] - self._attrs.update( + self._attr_state = POLLUTANT_LABELS[symbol] + self._attr_extra_state_attributes.update( { ATTR_POLLUTANT_SYMBOL: symbol, ATTR_POLLUTANT_UNIT: POLLUTANT_UNITS[symbol], @@ -206,30 +234,29 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): ) if self._config_entry.options[CONF_SHOW_ON_MAP]: - self._attrs[ATTR_LATITUDE] = latitude - self._attrs[ATTR_LONGITUDE] = longitude - self._attrs.pop("lati", None) - self._attrs.pop("long", None) + self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude + self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude + self._attr_extra_state_attributes.pop("lati", None) + self._attr_extra_state_attributes.pop("long", None) else: - self._attrs["lati"] = latitude - self._attrs["long"] = longitude - self._attrs.pop(ATTR_LATITUDE, None) - self._attrs.pop(ATTR_LONGITUDE, None) + self._attr_extra_state_attributes["lati"] = latitude + self._attr_extra_state_attributes["long"] = longitude + self._attr_extra_state_attributes.pop(ATTR_LATITUDE, None) + self._attr_extra_state_attributes.pop(ATTR_LONGITUDE, None) class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" - def __init__(self, coordinator, kind, name, device_class, unit): + def __init__(self, coordinator, kind, name, device_class, icon, unit): """Initialize.""" super().__init__(coordinator) + self._attr_device_class = device_class + self._attr_icon = icon + self._attr_unit_of_measurement = unit self._kind = kind self._name = name - self._state = None - - self._attr_device_class = device_class - self._attr_unit_of_measurement = unit @property def device_info(self): @@ -251,11 +278,6 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): node_name = self.coordinator.data["settings"]["node_name"] return f"{node_name} Node/Pro: {self._name}" - @property - def state(self): - """Return the state.""" - return self._state - @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" @@ -264,9 +286,26 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): @callback def update_from_latest_data(self): """Update the entity from the latest data.""" - if self._kind == SENSOR_KIND_BATTERY_LEVEL: - self._state = self.coordinator.data["status"]["battery"] + if self._kind == SENSOR_KIND_AQI: + if self.coordinator.data["settings"]["is_aqi_usa"]: + self._attr_state = self.coordinator.data["measurements"]["aqi_us"] + else: + self._attr_state = self.coordinator.data["measurements"]["aqi_cn"] + elif self._kind == SENSOR_KIND_BATTERY_LEVEL: + self._attr_state = self.coordinator.data["status"]["battery"] + elif self._kind == SENSOR_KIND_CO2: + self._attr_state = self.coordinator.data["measurements"].get("co2") elif self._kind == SENSOR_KIND_HUMIDITY: - self._state = self.coordinator.data["measurements"].get("humidity") + self._attr_state = self.coordinator.data["measurements"].get("humidity") + elif self._kind == SENSOR_KIND_PM_0_1: + self._attr_state = self.coordinator.data["measurements"].get("pm0_1") + elif self._kind == SENSOR_KIND_PM_1_0: + self._attr_state = self.coordinator.data["measurements"].get("pm1_0") + elif self._kind == SENSOR_KIND_PM_2_5: + self._attr_state = self.coordinator.data["measurements"].get("pm2_5") elif self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self.coordinator.data["measurements"].get("temperature_C") + self._attr_state = self.coordinator.data["measurements"].get( + "temperature_C" + ) + elif self._kind == SENSOR_KIND_VOC: + self._attr_state = self.coordinator.data["measurements"].get("voc") From abc9b01ede0c3703b0bbb55f63ed2efe5f37d250 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 5 Jul 2021 00:09:10 +0000 Subject: [PATCH 074/818] [ci skip] Translation update --- .../accuweather/translations/he.json | 16 ++++- .../components/cast/translations/he.json | 9 ++- .../cloudflare/translations/he.json | 6 ++ .../components/coinbase/translations/he.json | 24 +++++++ .../devolo_home_control/translations/he.json | 3 +- .../components/dsmr/translations/he.json | 33 ++++++++- .../forecast_solar/translations/he.json | 13 ++++ .../freedompro/translations/he.json | 18 +++++ .../growatt_server/translations/ru.json | 2 +- .../components/kodi/translations/he.json | 17 ++++- .../mobile_app/translations/he.json | 10 +++ .../components/mqtt/translations/he.json | 6 ++ .../components/mysensors/translations/he.json | 6 ++ .../nmap_tracker/translations/he.json | 37 ++++++++++ .../nmap_tracker/translations/nl.json | 4 +- .../nmap_tracker/translations/pl.json | 4 +- .../components/onvif/translations/he.json | 10 +++ .../components/ozw/translations/he.json | 1 + .../components/rpi_power/translations/he.json | 3 +- .../components/select/translations/he.json | 3 + .../components/tasmota/translations/he.json | 6 ++ .../xiaomi_miio/translations/he.json | 69 +++++++++++++++++-- .../components/zwave_js/translations/he.json | 30 ++++++++ 23 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/he.json create mode 100644 homeassistant/components/forecast_solar/translations/he.json create mode 100644 homeassistant/components/freedompro/translations/he.json create mode 100644 homeassistant/components/mobile_app/translations/he.json create mode 100644 homeassistant/components/nmap_tracker/translations/he.json create mode 100644 homeassistant/components/select/translations/he.json diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json index 869e00ca064..219ce00872f 100644 --- a/homeassistant/components/accuweather/translations/he.json +++ b/homeassistant/components/accuweather/translations/he.json @@ -14,8 +14,22 @@ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd" - } + }, + "title": "AccuWeather" } } + }, + "options": { + "step": { + "user": { + "description": "\u05d1\u05e9\u05dc \u05de\u05d2\u05d1\u05dc\u05d5\u05ea \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05d7\u05d9\u05e0\u05de\u05d9\u05ea \u05e9\u05dc \u05de\u05e4\u05ea\u05d7 \u05d4-API \u05e9\u05dc AccuWeather, \u05db\u05d0\u05e9\u05e8 \u05ea\u05e4\u05e2\u05d9\u05dc \u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d0\u05d5\u05d5\u05d9\u05e8, \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d9\u05d1\u05d5\u05e6\u05e2\u05d5 \u05db\u05dc 80 \u05d3\u05e7\u05d5\u05ea \u05d1\u05de\u05e7\u05d5\u05dd \u05db\u05dc 40 \u05d3\u05e7\u05d5\u05ea.", + "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u05d4\u05e9\u05d2\u05ea \u05e9\u05e8\u05ea AccuWeather" + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index 1d3af8b2718..e361c092b09 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -11,7 +11,8 @@ "data": { "known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd" }, - "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc." + "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc.", + "title": "\u05ea\u05e6\u05d5\u05e8\u05ea Google Cast" }, "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Google Cast?" @@ -23,11 +24,15 @@ "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd." }, "step": { + "advanced_options": { + "title": "\u05ea\u05e6\u05d5\u05e8\u05d4 \u05de\u05ea\u05e7\u05d3\u05de\u05ea \u05e9\u05dc Google Cast" + }, "basic_options": { "data": { "known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd" }, - "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc." + "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc.", + "title": "\u05ea\u05e6\u05d5\u05e8\u05ea Google Cast" } } } diff --git a/homeassistant/components/cloudflare/translations/he.json b/homeassistant/components/cloudflare/translations/he.json index 445cf45325d..fb0a20a223b 100644 --- a/homeassistant/components/cloudflare/translations/he.json +++ b/homeassistant/components/cloudflare/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, @@ -11,6 +12,11 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } + }, "records": { "data": { "records": "\u05e8\u05e9\u05d5\u05de\u05d5\u05ea" diff --git a/homeassistant/components/coinbase/translations/he.json b/homeassistant/components/coinbase/translations/he.json new file mode 100644 index 00000000000..3446e8e5ede --- /dev/null +++ b/homeassistant/components/coinbase/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + }, + "options": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index 903818bf429..f9ab82881a1 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" diff --git a/homeassistant/components/dsmr/translations/he.json b/homeassistant/components/dsmr/translations/he.json index cdb921611c4..8e2195dbc42 100644 --- a/homeassistant/components/dsmr/translations/he.json +++ b/homeassistant/components/dsmr/translations/he.json @@ -1,7 +1,38 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "setup_network": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + }, + "setup_serial": { + "data": { + "port": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df" + }, + "title": "\u05d4\u05ea\u05e7\u05df" + }, + "setup_serial_manual_path": { + "data": { + "port": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + }, + "title": "\u05e0\u05ea\u05d9\u05d1" + }, + "user": { + "data": { + "type": "\u05e1\u05d5\u05d2 \u05d7\u05d9\u05d1\u05d5\u05e8" + }, + "title": "\u05d1\u05d7\u05e8 \u05e1\u05d5\u05d2 \u05d7\u05d9\u05d1\u05d5\u05e8" + } } } } \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/he.json b/homeassistant/components/forecast_solar/translations/he.json new file mode 100644 index 00000000000..99eeb837dc3 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/he.json b/homeassistant/components/freedompro/translations/he.json new file mode 100644 index 00000000000..b4bb1b26bfe --- /dev/null +++ b/homeassistant/components/freedompro/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json index 02557515f97..7d866ba09b0 100644 --- a/homeassistant/components/growatt_server/translations/ru.json +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -23,5 +23,5 @@ } } }, - "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Growatt" + "title": "Growatt" } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json index 07d8838f200..04e02ee88a0 100644 --- a/homeassistant/components/kodi/translations/he.json +++ b/homeassistant/components/kodi/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "no_uuid": "\u05dc\u05de\u05d5\u05e4\u05e2 Kodi \u05d0\u05d9\u05df \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9. \u05d4\u05e1\u05d9\u05d1\u05d4 \u05dc\u05db\u05da \u05d4\u05d9\u05d0 \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05d2\u05e8\u05e1\u05ea \u05e7\u05d5\u05d3\u05d9 \u05d9\u05e9\u05e0\u05d4 (17.x \u05d5\u05de\u05d8\u05d4). \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea\u05da \u05dc\u05e7\u05d1\u05d5\u05e2 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 \u05d0\u05d5 \u05dc\u05e9\u05d3\u05e8\u05d2 \u05dc\u05d2\u05d9\u05e8\u05e1\u05ea Kodi \u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d9\u05d5\u05ea\u05e8.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { @@ -20,18 +21,30 @@ }, "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05d1-Kodi. \u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05dd \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd." }, + "discovery_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9 ((`{name}`) \u05dc-Home Assistant?", + "title": "\u05d2\u05d9\u05dc\u05d4 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4", "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL" - } + }, + "description": "\u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05d7\u05d9\u05d1\u05d5\u05e8 Kodi. \u05d9\u05e9 \u05dc\u05d4\u05e7\u05e4\u05d9\u05d3 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05ea \"\u05d0\u05e4\u05e9\u05e8 \u05e9\u05dc\u05d9\u05d8\u05d4 \u05d1\u05e7\u05d5\u05d3\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea HTTP\" \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd." }, "ws_port": { "data": { "ws_port": "\u05e4\u05ea\u05d7\u05d4" - } + }, + "description": "\u05d9\u05e6\u05d9\u05d0\u05ea WebSocket (\u05e0\u05e7\u05e8\u05d0\u05ea \u05dc\u05e4\u05e2\u05de\u05d9\u05dd \u05d1\u05e7\u05d5\u05d3\u05d9 \u05d9\u05e6\u05d9\u05d0\u05ea TCP). \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d3\u05e8\u05da WebSocket, \u05e2\u05dc\u05d9\u05da \u05dc\u05d0\u05e4\u05e9\u05e8 \"\u05d0\u05e4\u05e9\u05e8 \u05dc\u05ea\u05d5\u05db\u05e0\u05d9\u05d5\u05ea ... \u05dc\u05e9\u05dc\u05d5\u05d8 \u05d1\u05e7\u05d5\u05d3\u05d9\" \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd. \u05d0\u05dd WebSocket \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc, \u05d4\u05e1\u05e8 \u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4 \u05d5\u05d4\u05e9\u05d0\u05d9\u05e8 \u05e8\u05d9\u05e7." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} \u05d4\u05ea\u05d1\u05e7\u05e9 \u05dc\u05db\u05d1\u05d5\u05ea", + "turn_on": "{entity_name} \u05d4\u05ea\u05d1\u05e7\u05e9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/he.json b/homeassistant/components/mobile_app/translations/he.json new file mode 100644 index 00000000000..e213f54137c --- /dev/null +++ b/homeassistant/components/mobile_app/translations/he.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05e8\u05db\u05d9\u05d1 \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3?" + } + } + }, + "title": "\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3" +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index ef628fb799b..f0c156b5fde 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -16,6 +16,10 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da." + }, + "hassio_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4-Home Assistant \u05db\u05da \u05e9\u05ea\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT \u05d4\u05de\u05e1\u05d5\u05e4\u05e7 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05d4\u05d4\u05e8\u05d7\u05d1\u05d4 {addon}?", + "title": "MQTT \u05d1\u05e8\u05d5\u05e7\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Home Assistant" } } }, @@ -30,9 +34,11 @@ "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da.", "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05d5\u05d5\u05da" }, "options": { + "description": "\u05d2\u05d9\u05dc\u05d5\u05d9 - \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 \u05de\u05d5\u05e4\u05e2\u05dc (\u05de\u05d5\u05de\u05dc\u05e5), Home Assistant \u05d9\u05d2\u05dc\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d5\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05de\u05e4\u05e8\u05e1\u05de\u05d9\u05dd \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea\u05dd \u05d1\u05de\u05ea\u05d5\u05d5\u05da MQTT. \u05d0\u05dd \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc, \u05db\u05dc \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d7\u05d9\u05d9\u05d1\u05ea \u05dc\u05d4\u05d9\u05e2\u05e9\u05d5\u05ea \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05dc\u05d9\u05d3\u05d4 \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05de\u05ea\u05d7\u05d1\u05e8 (\u05de\u05d7\u05d3\u05e9) \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05e8\u05e6\u05d5\u05df - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05e8\u05e6\u05d5\u05df \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05d9\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d4\u05e7\u05e9\u05e8 \u05e9\u05dc\u05d5 \u05dc\u05de\u05ea\u05d5\u05d5\u05da, \u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc \u05db\u05d9\u05d1\u05d5\u05d9 \u05e9\u05dc Home Assistant) \u05d5\u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05dc\u05d0 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc Home Assistant \u05de\u05ea\u05e8\u05e1\u05e7 \u05d0\u05d5 \u05de\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8 \u05d4\u05e8\u05e9\u05ea \u05e9\u05dc\u05d5).", "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" } } diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json index 7ded555edb8..587c3ae9132 100644 --- a/homeassistant/components/mysensors/translations/he.json +++ b/homeassistant/components/mysensors/translations/he.json @@ -10,7 +10,13 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "gw_mqtt": { + "description": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05e9\u05e2\u05e8 MQTT" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/he.json b/homeassistant/components/nmap_tracker/translations/he.json new file mode 100644 index 00000000000..0296c7103b6 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/he.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "user": { + "data": { + "exclude": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e4\u05e1\u05d9\u05e7) \u05e9\u05dc\u05d0 \u05d9\u05d9\u05db\u05dc\u05dc\u05d5 \u05d1\u05e1\u05e8\u05d9\u05e7\u05d4", + "home_interval": "\u05de\u05e1\u05e4\u05e8 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9 \u05e9\u05dc \u05d3\u05e7\u05d5\u05ea \u05d1\u05d9\u05df \u05e1\u05e8\u05d9\u05e7\u05d5\u05ea \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e4\u05e2\u05d9\u05dc\u05d9\u05dd (\u05e9\u05d9\u05de\u05d5\u05e8 \u05e1\u05d5\u05dc\u05dc\u05d4)", + "hosts": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7) \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", + "scan_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e1\u05e8\u05d9\u05e7\u05d4 \u05d2\u05d5\u05dc\u05de\u05d9\u05d5\u05ea \u05d4\u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 Nmap" + }, + "description": "\u05e7\u05d1\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05e9\u05d9\u05e1\u05e8\u05e7\u05d5 \u05e2\u05dc \u05d9\u05d3\u05d9 Nmap. \u05db\u05ea\u05d5\u05d1\u05ea \u05e8\u05e9\u05ea \u05d5\u05d0\u05d9 \u05d4\u05db\u05dc\u05dc\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05d4\u05d9\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP (192.168.1.1), \u05e8\u05e9\u05ea\u05d5\u05ea IP (192.168.0.0/24) \u05d0\u05d5 \u05d8\u05d5\u05d5\u05d7\u05d9 IP (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "init": { + "data": { + "exclude": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e4\u05e1\u05d9\u05e7) \u05e9\u05dc\u05d0 \u05d9\u05d9\u05db\u05dc\u05dc\u05d5 \u05d1\u05e1\u05e8\u05d9\u05e7\u05d4", + "home_interval": "\u05de\u05e1\u05e4\u05e8 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9 \u05e9\u05dc \u05d3\u05e7\u05d5\u05ea \u05d1\u05d9\u05df \u05e1\u05e8\u05d9\u05e7\u05d5\u05ea \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e4\u05e2\u05d9\u05dc\u05d9\u05dd (\u05e9\u05d9\u05de\u05d5\u05e8 \u05e1\u05d5\u05dc\u05dc\u05d4)", + "hosts": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7) \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", + "scan_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e1\u05e8\u05d9\u05e7\u05d4 \u05d2\u05d5\u05dc\u05de\u05d9\u05d5\u05ea \u05d4\u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 Nmap" + }, + "description": "\u05e7\u05d1\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05e9\u05d9\u05e1\u05e8\u05e7\u05d5 \u05e2\u05dc \u05d9\u05d3\u05d9 Nmap. \u05db\u05ea\u05d5\u05d1\u05ea \u05e8\u05e9\u05ea \u05d5\u05d0\u05d9 \u05d4\u05db\u05dc\u05dc\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05d4\u05d9\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP (192.168.1.1), \u05e8\u05e9\u05ea\u05d5\u05ea IP (192.168.0.0/24) \u05d0\u05d5 \u05d8\u05d5\u05d5\u05d7\u05d9 IP (192.168.1.0-32)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json index b1812db53b3..e9675dfc328 100644 --- a/homeassistant/components/nmap_tracker/translations/nl.json +++ b/homeassistant/components/nmap_tracker/translations/nl.json @@ -28,7 +28,9 @@ "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen", "home_interval": "Minimum aantal minuten tussen scans van actieve apparaten (batterij sparen)", "hosts": "Netwerkadressen (gescheiden door komma's) om te scannen", - "scan_options": "Ruwe configureerbare scanopties voor Nmap" + "interval_seconds": "Scaninterval", + "scan_options": "Ruwe configureerbare scanopties voor Nmap", + "track_new_devices": "Volg nieuwe apparaten" }, "description": "Configureer hosts die moeten worden gescand door Nmap. Netwerkadres en uitsluitingen kunnen IP-adressen (192.168.1.1), IP-netwerken (192.168.0.0/24) of IP-bereiken (192.168.1.0-32) zijn." } diff --git a/homeassistant/components/nmap_tracker/translations/pl.json b/homeassistant/components/nmap_tracker/translations/pl.json index c0fbd9a3a70..dc16816609c 100644 --- a/homeassistant/components/nmap_tracker/translations/pl.json +++ b/homeassistant/components/nmap_tracker/translations/pl.json @@ -28,7 +28,9 @@ "exclude": "Adresy sieciowe (rozdzielone przecinkami) do wykluczenia ze skanowania", "home_interval": "Minimalna liczba minut mi\u0119dzy skanami aktywnych urz\u0105dze\u0144 (oszcz\u0119dzanie baterii)", "hosts": "Adresy sieciowe (oddzielone przecinkami) do skanowania", - "scan_options": "Surowe konfigurowalne opcje skanowania dla Nmap" + "interval_seconds": "Cz\u0119stotliwo\u015b\u0107 skanowania", + "scan_options": "Surowe konfigurowalne opcje skanowania dla Nmap", + "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" }, "description": "Skonfiguruj hosta do skanowania przez Nmap. Adresy sieciowe i te wykluczone mog\u0105 by\u0107 adresami IP (192.168.1.1), sieciami IP (192.168.0.0/24) lub zakresami IP (192.168.1.0-32)." } diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index ec9da5b556e..290effbc48e 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -16,6 +16,16 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } }, + "configure": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df ONVIF" + }, "configure_profile": { "data": { "include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4" diff --git a/homeassistant/components/ozw/translations/he.json b/homeassistant/components/ozw/translations/he.json index 10f3cb6d722..021d34db25a 100644 --- a/homeassistant/components/ozw/translations/he.json +++ b/homeassistant/components/ozw/translations/he.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/rpi_power/translations/he.json b/homeassistant/components/rpi_power/translations/he.json index a18f311e43a..a4e4e475087 100644 --- a/homeassistant/components/rpi_power/translations/he.json +++ b/homeassistant/components/rpi_power/translations/he.json @@ -8,5 +8,6 @@ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" } } - } + }, + "title": "\u05d1\u05d5\u05d3\u05e7 \u05d0\u05e1\u05e4\u05e7\u05ea \u05d4\u05d7\u05e9\u05de\u05dc \u05e9\u05dc \u05e8\u05e1\u05e4\u05d1\u05e8\u05d9 \u05e4\u05d0\u05d9" } \ No newline at end of file diff --git a/homeassistant/components/select/translations/he.json b/homeassistant/components/select/translations/he.json new file mode 100644 index 00000000000..7f2ff684474 --- /dev/null +++ b/homeassistant/components/select/translations/he.json @@ -0,0 +1,3 @@ +{ + "title": "\u05d1\u05d7\u05e8" +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/he.json b/homeassistant/components/tasmota/translations/he.json index 7853c226b33..eefc72310d4 100644 --- a/homeassistant/components/tasmota/translations/he.json +++ b/homeassistant/components/tasmota/translations/he.json @@ -3,8 +3,14 @@ "abort": { "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "error": { + "invalid_discovery_topic": "\u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05e4\u05d9 \u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e0\u05d5\u05e9\u05d0 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea." + }, "step": { "config": { + "data": { + "discovery_prefix": "\u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05e4\u05d9 \u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e0\u05d5\u05e9\u05d0" + }, "description": "\u05d0\u05e0\u05d0 \u05d4\u05db\u05e0\u05e1 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea Tasmota.", "title": "Tasmota" }, diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 9eb4ffc0bb7..36f54e79361 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -3,34 +3,95 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "incomplete_info": "\u05de\u05d9\u05d3\u05e2 \u05dc\u05d0 \u05e9\u05dc\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df, \u05dc\u05d0 \u05e1\u05d5\u05e4\u05e7\u05d5 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df.", + "not_xiaomi_miio": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da (\u05e2\u05d3\u05d9\u05d9\u05df) \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5.", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "cloud_credentials_incomplete": "\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e2\u05e0\u05df \u05d0\u05d9\u05e0\u05dd \u05de\u05dc\u05d0\u05d9\u05dd, \u05e0\u05d0 \u05dc\u05de\u05dc\u05d0 \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9, \u05e1\u05d9\u05e1\u05de\u05d4 \u05d5\u05de\u05d3\u05d9\u05e0\u05d4", + "cloud_login_error": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5, \u05e0\u05d0 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9\u05dd.", + "cloud_no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05d7\u05e9\u05d1\u05d5\u05df \u05d4\u05e2\u05e0\u05df \u05d4\u05d6\u05d4 \u05e9\u05dc \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5.", + "no_device_selected": "\u05dc\u05d0 \u05e0\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df, \u05e0\u05d0 \u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df \u05d0\u05d7\u05d3.", + "unknown_device": "\u05d3\u05d2\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05d9\u05d3\u05d5\u05e2, \u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d6\u05e8\u05d9\u05de\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "\u05de\u05d3\u05d9\u05e0\u05ea \u05e9\u05e8\u05ea \u05e2\u05e0\u05df", + "cloud_password": "\u05e1\u05d9\u05e1\u05de\u05ea \u05e2\u05e0\u05df", + "cloud_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e2\u05e0\u05df", + "manual": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea (\u05dc\u05d0 \u05de\u05d5\u05de\u05dc\u05e5)" + }, + "description": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5 \u05dc\u05e2\u05e0\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5, \u05e8\u05d0\u05d5 https://www.openhab.org/addons/bindings/miio/#country-servers \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e9\u05e8\u05ea \u05d4\u05e2\u05e0\u05df.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + }, + "connect": { + "data": { + "model": "\u05d3\u05d2\u05dd \u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05e8 \u05d9\u05d3\u05e0\u05d9\u05ea \u05d0\u05ea \u05d3\u05d2\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05d4\u05d3\u05d2\u05de\u05d9\u05dd \u05d4\u05e0\u05ea\u05de\u05db\u05d9\u05dd.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + }, "device": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "model": "\u05d3\u05d2\u05dd \u05d4\u05ea\u05e7\u05df (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "name": "\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" - } + }, + "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" }, "gateway": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "name": "\u05e9\u05dd \u05d4\u05e9\u05e2\u05e8", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" }, - "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara." + "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" }, "manual": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" - } + }, + "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" }, "reauth_confirm": { + "description": "\u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05e6\u05e8\u05d9\u05db\u05d4 \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05e0\u05d9\u05dd \u05d0\u05d5 \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e2\u05e0\u05df \u05d7\u05e1\u05e8\u05d9\u05dd.", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "select": { + "data": { + "select_device": "\u05d4\u05ea\u05e7\u05df \u05de\u05d9\u05d5" + }, + "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + }, + "user": { + "data": { + "gateway": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + }, + "description": "\u05d1\u05d7\u05e8 \u05dc\u05d0\u05d9\u05d6\u05d4 \u05d4\u05ea\u05e7\u05df \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8.", + "title": "\u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5" + } + } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e2\u05e0\u05df \u05d0\u05d9\u05e0\u05dd \u05de\u05dc\u05d0\u05d9\u05dd, \u05e0\u05d0 \u05de\u05dc\u05d0 \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9, \u05e1\u05d9\u05e1\u05de\u05d4 \u05d5\u05de\u05d3\u05d9\u05e0\u05d4" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e2\u05e0\u05df \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05ea\u05ea-\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05de\u05d7\u05d5\u05d1\u05e8\u05d9\u05dd" + }, + "description": "\u05e6\u05d9\u05d9\u05df \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9\u05d5\u05ea", + "title": "\u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5" } } } diff --git a/homeassistant/components/zwave_js/translations/he.json b/homeassistant/components/zwave_js/translations/he.json index 4dbfc33457f..32a2ff96298 100644 --- a/homeassistant/components/zwave_js/translations/he.json +++ b/homeassistant/components/zwave_js/translations/he.json @@ -28,5 +28,35 @@ "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" } } + }, + "options": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "configure_addon": { + "data": { + "log_level": "\u05e8\u05de\u05ea \u05d9\u05d5\u05de\u05df \u05e8\u05d9\u05e9\u05d5\u05dd", + "network_key": "\u05de\u05e4\u05ea\u05d7 \u05e8\u05e9\u05ea", + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "manual": { + "data": { + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS" + }, + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS?" + } + } } } \ No newline at end of file From 3a2a688170e8bb4dffeb64d2a28a53202b703119 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 01:51:10 -0400 Subject: [PATCH 075/818] Use entity class attributes for ambiclimate (#52521) * Use entity class attributes for ambiclimate * tweak * move some stuff --- .../components/ambiclimate/__init__.py | 4 +- .../components/ambiclimate/climate.py | 93 ++++--------------- .../components/ambiclimate/config_flow.py | 2 +- 3 files changed, 23 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index e9247b9fd73..ac6334638a4 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -20,7 +20,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass, config) -> bool: """Set up Ambiclimate components.""" if DOMAIN not in config: return True @@ -34,7 +34,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass, entry) -> bool: """Set up Ambiclimate from a config entry.""" hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "climate") diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 93b38974464..a49253af6bc 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -137,92 +137,33 @@ async def async_setup_entry(hass, entry, async_add_entities): class AmbiclimateEntity(ClimateEntity): """Representation of a Ambiclimate Thermostat device.""" + _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = 1 + _attr_supported_features = SUPPORT_FLAGS + _attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + def __init__(self, heater, store): """Initialize the thermostat.""" self._heater = heater self._store = store - self._data = {} - - @property - def unique_id(self): - """Return a unique ID.""" - return self._heater.device_id - - @property - def name(self): - """Return the name of the entity.""" - return self._heater.name - - @property - def device_info(self): - """Return the device info.""" - return { + self._attr_unique_id = self._heater.device_id + self._attr_name = self._heater.name + self._attr_device_info = { "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, "manufacturer": "Ambiclimate", } + self._attr_min_temp = self._heater.get_min_temp() + self._attr_max_temp = self._heater.get_max_temp() - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - - @property - def target_temperature(self): - """Return the target temperature.""" - return self._data.get("target_temperature") - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 1 - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._data.get("temperature") - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._data.get("humidity") - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self._heater.get_min_temp() - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self._heater.get_max_temp() - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] - - @property - def hvac_mode(self): - """Return current operation.""" - if self._data.get("power", "").lower() == "on": - return HVAC_MODE_HEAT - - return HVAC_MODE_OFF - - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return await self._heater.set_target_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: await self._heater.turn_on() @@ -230,7 +171,7 @@ class AmbiclimateEntity(ClimateEntity): if hvac_mode == HVAC_MODE_OFF: await self._heater.turn_off() - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" try: token_info = await self._heater.control.refresh_access_token() @@ -241,4 +182,10 @@ class AmbiclimateEntity(ClimateEntity): if token_info: await self._store.async_save(token_info) - self._data = await self._heater.update_device() + data = await self._heater.update_device() + self._attr_target_temperature = data.get("target_temperature") + self._attr_current_temperature = data.get("temperature") + self._attr_current_humidity = data.get("humidity") + self._attr_hvac_mode = ( + HVAC_MODE_HEAT if data.get("power", "").lower() == "on" else HVAC_MODE_OFF + ) diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 7ef0c5439aa..7f9ff9e5d09 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -139,7 +139,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView): url = AUTH_CALLBACK_PATH name = AUTH_CALLBACK_NAME - async def get(self, request): + async def get(self, request) -> str: """Receive authorization token.""" code = request.query.get("code") if code is None: From 4463d507118a796e8f2163c666f397c7a44cae14 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 01:53:16 -0400 Subject: [PATCH 076/818] Use entity class attributes for aemet (#52499) --- homeassistant/components/aemet/sensor.py | 41 +++++------------------ homeassistant/components/aemet/weather.py | 35 +++++-------------- 2 files changed, 17 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index de7b06347c3..3fd0769cb00 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -66,6 +66,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AbstractAemetSensor(CoordinatorEntity, SensorEntity): """Abstract class for an AEMET OpenData sensor.""" + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + def __init__( self, name, @@ -80,33 +82,10 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity): self._unique_id = unique_id self._sensor_type = sensor_type self._sensor_name = sensor_configuration[SENSOR_NAME] - self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) - self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._sensor_name}" - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def device_class(self): - """Return the device_class.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_name = f"{self._name} {self._sensor_name}" + self._attr_unique_id = self._unique_id + self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) + self._attr_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) class AemetSensor(AbstractAemetSensor): @@ -150,11 +129,9 @@ class AemetForecastSensor(AbstractAemetSensor): ) self._weather_coordinator = weather_coordinator self._forecast_mode = forecast_mode - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._forecast_mode == FORECAST_MODE_DAILY + self._attr_entity_registry_enabled_default = ( + self._forecast_mode == FORECAST_MODE_DAILY + ) @property def state(self): diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index e54a297cc09..07bb0bfba83 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -39,6 +39,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AemetWeather(CoordinatorEntity, WeatherEntity): """Implementation of an AEMET OpenData sensor.""" + _attr_attribution = ATTRIBUTION + _attr_temperature_unit = TEMP_CELSIUS + def __init__( self, name, @@ -48,25 +51,18 @@ class AemetWeather(CoordinatorEntity, WeatherEntity): ): """Initialize the sensor.""" super().__init__(coordinator) - self._name = name - self._unique_id = unique_id self._forecast_mode = forecast_mode - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION + self._attr_entity_registry_enabled_default = ( + self._forecast_mode == FORECAST_MODE_DAILY + ) + self._attr_name = name + self._attr_unique_id = unique_id @property def condition(self): """Return the current condition.""" return self.coordinator.data[ATTR_API_CONDITION] - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._forecast_mode == FORECAST_MODE_DAILY - @property def forecast(self): """Return the forecast array.""" @@ -77,11 +73,6 @@ class AemetWeather(CoordinatorEntity, WeatherEntity): """Return the humidity.""" return self.coordinator.data[ATTR_API_HUMIDITY] - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def pressure(self): """Return the pressure.""" @@ -92,16 +83,6 @@ class AemetWeather(CoordinatorEntity, WeatherEntity): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._unique_id - @property def wind_bearing(self): """Return the temperature.""" From e2b89e4650dc7e6bbd24c54bd1e326d579216c0e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 03:37:09 -0400 Subject: [PATCH 077/818] Use entity class attributes for alpha_vantage (#52520) * Use entity class attributes for alpha_vantage * tweak * clean up --- .../components/alpha_vantage/sensor.py | 108 ++++++------------ 1 file changed, 34 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 0788772a45b..512de247ff2 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -110,48 +110,27 @@ class AlphaVantageSensor(SensorEntity): def __init__(self, timeseries, symbol): """Initialize the sensor.""" self._symbol = symbol[CONF_SYMBOL] - self._name = symbol.get(CONF_NAME, self._symbol) + self._attr_name = symbol.get(CONF_NAME, self._symbol) self._timeseries = timeseries - self.values = None - self._unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) - self._icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def state(self): - """Return the state of the sensor.""" - return self.values["1. open"] - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if self.values is not None: - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_CLOSE: self.values["4. close"], - ATTR_HIGH: self.values["2. high"], - ATTR_LOW: self.values["3. low"], - } - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon + self._attr_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) + self._attr_icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) def update(self): """Get the latest data and updates the states.""" _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) - self.values = next(iter(all_values.values())) + values = next(iter(all_values.values())) + self._attr_state = values["1. open"] + self._attr_extra_state_attributes = ( + { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_CLOSE: values["4. close"], + ATTR_HIGH: values["2. high"], + ATTR_LOW: values["3. low"], + } + if values is not None + else None + ) _LOGGER.debug("Received new values for symbol %s", self._symbol) @@ -163,43 +142,13 @@ class AlphaVantageForeignExchange(SensorEntity): self._foreign_exchange = foreign_exchange self._from_currency = config[CONF_FROM] self._to_currency = config[CONF_TO] - if CONF_NAME in config: - self._name = config.get(CONF_NAME) - else: - self._name = f"{self._to_currency}/{self._from_currency}" - self._unit_of_measurement = self._to_currency - self._icon = ICONS.get(self._from_currency, "USD") - self.values = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def state(self): - """Return the state of the sensor.""" - return round(float(self.values["5. Exchange Rate"]), 4) - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if self.values is not None: - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - CONF_FROM: self._from_currency, - CONF_TO: self._to_currency, - } + self._attr_name = ( + config.get(CONF_NAME) + if CONF_NAME in config + else f"{self._to_currency}/{self._from_currency}" + ) + self._attr_icon = ICONS.get(self._from_currency, "USD") + self._attr_unit_of_measurement = self._to_currency def update(self): """Get the latest data and updates the states.""" @@ -208,9 +157,20 @@ class AlphaVantageForeignExchange(SensorEntity): self._from_currency, self._to_currency, ) - self.values, _ = self._foreign_exchange.get_currency_exchange_rate( + values, _ = self._foreign_exchange.get_currency_exchange_rate( from_currency=self._from_currency, to_currency=self._to_currency ) + self._attr_state = round(float(values["5. Exchange Rate"]), 4) + self._attr_extra_state_attributes = ( + { + ATTR_ATTRIBUTION: ATTRIBUTION, + CONF_FROM: self._from_currency, + CONF_TO: self._to_currency, + } + if values is not None + else None + ) + _LOGGER.debug( "Received new data for forex %s - %s", self._from_currency, From e9453bb344ca639e43be16e14895325c42fb9dce Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 03:42:39 -0400 Subject: [PATCH 078/818] Use entity class attributes for alert (#52518) --- homeassistant/components/alert/__init__.py | 26 ++++++++-------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 73bea193394..69edb3ee001 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -152,6 +152,8 @@ async def async_setup(hass, config): class Alert(ToggleEntity): """Representation of an alert.""" + _attr_should_poll = False + def __init__( self, hass, @@ -170,7 +172,7 @@ class Alert(ToggleEntity): ): """Initialize the alert.""" self.hass = hass - self._name = name + self._attr_name = name self._alert_state = state self._skip_first = skip_first self._data = data @@ -203,16 +205,6 @@ class Alert(ToggleEntity): hass, [watched_entity_id], self.watched_entity_change ) - @property - def name(self): - """Return the name of the alert.""" - return self._name - - @property - def should_poll(self): - """Home Assistant need not poll these entities.""" - return False - @property def state(self): """Return the alert status.""" @@ -235,7 +227,7 @@ class Alert(ToggleEntity): async def begin_alerting(self): """Begin the alert procedures.""" - _LOGGER.debug("Beginning Alert: %s", self._name) + _LOGGER.debug("Beginning Alert: %s", self._attr_name) self._ack = False self._firing = True self._next_delay = 0 @@ -249,7 +241,7 @@ class Alert(ToggleEntity): async def end_alerting(self): """End the alert procedures.""" - _LOGGER.debug("Ending Alert: %s", self._name) + _LOGGER.debug("Ending Alert: %s", self._attr_name) self._cancel() self._ack = False self._firing = False @@ -272,13 +264,13 @@ class Alert(ToggleEntity): return if not self._ack: - _LOGGER.info("Alerting: %s", self._name) + _LOGGER.info("Alerting: %s", self._attr_name) self._send_done_message = True if self._message_template is not None: message = self._message_template.async_render(parse_result=False) else: - message = self._name + message = self._attr_name await self._send_notification_message(message) await self._schedule_notify() @@ -314,13 +306,13 @@ class Alert(ToggleEntity): async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" - _LOGGER.debug("Reset Alert: %s", self._name) + _LOGGER.debug("Reset Alert: %s", self._attr_name) self._ack = False self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Async Acknowledge alert.""" - _LOGGER.debug("Acknowledged Alert: %s", self._name) + _LOGGER.debug("Acknowledged Alert: %s", self._attr_name) self._ack = True self.async_write_ha_state() From 0e7cd02d178b3972d0520a9e41eb241ae575a94b Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Mon, 5 Jul 2021 11:19:37 +0300 Subject: [PATCH 079/818] Add type checking and entity attributes for Fast.com (#52524) * Cleanup fast.com (typing, attrs) * Adress review * fixes --- .strict-typing | 1 + .../components/fastdotcom/__init__.py | 15 +++-- homeassistant/components/fastdotcom/sensor.py | 64 ++++++++----------- mypy.ini | 11 ++++ 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/.strict-typing b/.strict-typing index dbf25ac927e..f267925ad19 100644 --- a/.strict-typing +++ b/.strict-typing @@ -31,6 +31,7 @@ homeassistant.components.dnsip.* homeassistant.components.dsmr.* homeassistant.components.dunehd.* homeassistant.components.elgato.* +homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index e0a4782493e..f2424332a01 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,15 +1,20 @@ """Support for testing internet speed via Fast.com.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from fastdotcom import fast_com import voluptuous as vol from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType DOMAIN = "fastdotcom" DATA_UPDATED = f"{DOMAIN}_data_updated" @@ -35,7 +40,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Fast.com component.""" conf = config[DOMAIN] data = hass.data[DOMAIN] = SpeedtestData(hass) @@ -43,7 +48,7 @@ async def async_setup(hass, config): if not conf[CONF_MANUAL]: async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) - def update(call=None): + def update(service_call: ServiceCall | None = None) -> None: """Service call to manually update the data.""" data.update() @@ -57,12 +62,12 @@ async def async_setup(hass, config): class SpeedtestData: """Get the latest data from fast.com.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize the data object.""" - self.data = None + self.data: dict[str, Any] | None = None self._hass = hass - def update(self, now=None): + def update(self) -> None: """Get the latest data from fast.com.""" _LOGGER.debug("Executing fast.com speedtest") diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index b4406a4de95..14f63a99e5d 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -1,16 +1,27 @@ """Support for Fast.com internet speed testing sensor.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.sensor import SensorEntity from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_UPDATED, DOMAIN as FASTDOTCOM_DOMAIN ICON = "mdi:speedometer" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Fast.com sensor.""" async_add_entities([SpeedtestSensor(hass.data[FASTDOTCOM_DOMAIN])]) @@ -18,38 +29,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SpeedtestSensor(RestoreEntity, SensorEntity): """Implementation of a FAst.com sensor.""" - def __init__(self, speedtest_data): + _attr_name = "Fast.com Download" + _attr_unit_of_measurement = DATA_RATE_MEGABITS_PER_SECOND + _attr_icon = ICON + _attr_should_poll = False + _attr_state = None + + def __init__(self, speedtest_data: dict[str, Any]) -> None: """Initialize the sensor.""" - self._name = "Fast.com Download" - self.speedtest_client = speedtest_data - self._state = None + self._speedtest_data = speedtest_data - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return DATA_RATE_MEGABITS_PER_SECOND - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() @@ -62,15 +52,15 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): state = await self.async_get_last_state() if not state: return - self._state = state.state + self._attr_state = state.state - def update(self): + def update(self) -> None: """Get the latest data and update the states.""" - data = self.speedtest_client.data + data = self._speedtest_data.data # type: ignore[attr-defined] if data is None: return - self._state = data["download"] + self._attr_state = data["download"] @callback - def _schedule_immediate_update(self): + def _schedule_immediate_update(self) -> None: self.async_schedule_update_ha_state(True) diff --git a/mypy.ini b/mypy.ini index 78a42cdaaf4..7e2b6f24632 100644 --- a/mypy.ini +++ b/mypy.ini @@ -352,6 +352,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fastdotcom.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fitbit.*] check_untyped_defs = true disallow_incomplete_defs = true From cacd803a93643cb5bd9a6030441afc1a7918f9cc Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 5 Jul 2021 03:27:52 -0500 Subject: [PATCH 080/818] Enable basic typing for roku (#52478) * enable basic typing for roku * Update mypy.ini * Update media_player.py * Create coordinator.py * Update __init__.py * Update media_player.py * Update remote.py * Update media_player.py * Update coordinator.py * Update coordinator.py * Update remote.py * Update entity.py * Update coordinator.py * Update config_flow.py * Update entity.py * Update const.py * Update const.py * Update const.py * Update entity.py * Update entity.py * Update entity.py * Update test_media_player.py * Update test_remote.py --- homeassistant/components/roku/__init__.py | 48 +-------------- homeassistant/components/roku/config_flow.py | 3 +- homeassistant/components/roku/const.py | 5 -- homeassistant/components/roku/coordinator.py | 60 +++++++++++++++++++ homeassistant/components/roku/entity.py | 25 ++++---- homeassistant/components/roku/media_player.py | 48 ++++++++------- homeassistant/components/roku/remote.py | 5 +- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - tests/components/roku/test_media_player.py | 34 +++++------ tests/components/roku/test_remote.py | 6 +- 11 files changed, 127 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/roku/coordinator.py diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index bc85915f39a..55da7484aba 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,11 +1,9 @@ """Support for Roku.""" from __future__ import annotations -from datetime import timedelta import logging -from rokuecp import Roku, RokuConnectionError, RokuError -from rokuecp.models import Device +from rokuecp import RokuConnectionError, RokuError from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN @@ -13,16 +11,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.dt import utcnow from .const import DOMAIN +from .coordinator import RokuDataUpdateCoordinator CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] -SCAN_INTERVAL = timedelta(seconds=15) _LOGGER = logging.getLogger(__name__) @@ -63,42 +58,3 @@ def roku_exception_handler(func): _LOGGER.error("Invalid response from API: %s", error) return handler - - -class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): - """Class to manage fetching Roku data.""" - - def __init__( - self, - hass: HomeAssistant, - *, - host: str, - ) -> None: - """Initialize global Roku data updater.""" - self.roku = Roku(host=host, session=async_get_clientsession(hass)) - - self.full_update_interval = timedelta(minutes=15) - self.last_full_update = None - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - ) - - async def _async_update_data(self) -> Device: - """Fetch data from Roku.""" - full_update = self.last_full_update is None or utcnow() >= ( - self.last_full_update + self.full_update_interval - ) - - try: - data = await self.roku.update(full_update=full_update) - - if full_update: - self.last_full_update = utcnow() - - return data - except RokuError as error: - raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 470dccbe37f..b9e93b4f008 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -111,7 +112,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_ssdp(self, discovery_info: dict | None = None) -> FlowResult: + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index dc458c88cd0..1a1383dceb6 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -2,12 +2,7 @@ DOMAIN = "roku" # Attributes -ATTR_IDENTIFIERS = "identifiers" ATTR_KEYWORD = "keyword" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MODEL = "model" -ATTR_SOFTWARE_VERSION = "sw_version" -ATTR_SUGGESTED_AREA = "suggested_area" # Default Values DEFAULT_PORT = 8060 diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py new file mode 100644 index 00000000000..08766efa42d --- /dev/null +++ b/homeassistant/components/roku/coordinator.py @@ -0,0 +1,60 @@ +"""Coordinator for Roku.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import logging + +from rokuecp import Roku, RokuError +from rokuecp.models import Device + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import utcnow + +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(seconds=15) +_LOGGER = logging.getLogger(__name__) + + +class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): + """Class to manage fetching Roku data.""" + + last_full_update: datetime | None + roku: Roku + + def __init__( + self, + hass: HomeAssistant, + *, + host: str, + ) -> None: + """Initialize global Roku data updater.""" + self.roku = Roku(host=host, session=async_get_clientsession(hass)) + + self.full_update_interval = timedelta(minutes=15) + self.last_full_update = None + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> Device: + """Fetch data from Roku.""" + full_update = self.last_full_update is None or utcnow() >= ( + self.last_full_update + self.full_update_interval + ) + + try: + data = await self.roku.update(full_update=full_update) + + if full_update: + self.last_full_update = utcnow() + + return data + except RokuError as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index aefc335e64d..5dc58d4b387 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -1,24 +1,25 @@ """Base Entity for Roku.""" from __future__ import annotations -from homeassistant.const import ATTR_NAME +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RokuDataUpdateCoordinator -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_SOFTWARE_VERSION, - ATTR_SUGGESTED_AREA, - DOMAIN, -) +from .const import DOMAIN class RokuEntity(CoordinatorEntity): """Defines a base Roku entity.""" + coordinator: RokuDataUpdateCoordinator + def __init__( self, *, device_id: str, coordinator: RokuDataUpdateCoordinator ) -> None: @@ -34,9 +35,9 @@ class RokuEntity(CoordinatorEntity): return { ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.name, + ATTR_NAME: self.coordinator.data.info.name, ATTR_MANUFACTURER: self.coordinator.data.info.brand, ATTR_MODEL: self.coordinator.data.info.model_name, - ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, - ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location, + ATTR_SW_VERSION: self.coordinator.data.info.version, + "suggested_area": self.coordinator.data.info.device_location, } diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index dc0f2ff704c..bb9b4bfa37f 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,6 +1,7 @@ """Support for the Roku media player.""" from __future__ import annotations +import datetime as dt import logging import voluptuous as vol @@ -8,6 +9,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( DEVICE_CLASS_RECEIVER, DEVICE_CLASS_TV, + BrowseMedia, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( @@ -37,9 +39,10 @@ from homeassistant.const import ( from homeassistant.helpers import entity_platform from homeassistant.helpers.network import is_internal_request -from . import RokuDataUpdateCoordinator, roku_exception_handler +from . import roku_exception_handler from .browse_media import build_item_response, library_payload from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH +from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity _LOGGER = logging.getLogger(__name__) @@ -63,7 +66,7 @@ SEARCH_SCHEMA = {vol.Required(ATTR_KEYWORD): str} async def async_setup_entry(hass, entry, async_add_entities): """Set up the Roku config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) @@ -88,6 +91,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): self._attr_name = coordinator.data.info.name self._attr_unique_id = unique_id + self._attr_supported_features = SUPPORT_ROKU def _media_playback_trackable(self) -> bool: """Detect if we have enough media data to track playback.""" @@ -105,7 +109,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return DEVICE_CLASS_RECEIVER @property - def state(self) -> str: + def state(self) -> str | None: """Return the state of the device.""" if self.coordinator.data.state.standby: return STATE_STANDBY @@ -133,12 +137,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_ROKU - - @property - def media_content_type(self) -> str: + def media_content_type(self) -> str | None: """Content type of current playing media.""" if self.app_id is None or self.app_name in ("Power Saver", "Roku"): return None @@ -149,7 +148,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return MEDIA_TYPE_APP @property - def media_image_url(self) -> str: + def media_image_url(self) -> str | None: """Image url of current playing media.""" if self.app_id is None or self.app_name in ("Power Saver", "Roku"): return None @@ -157,7 +156,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return self.coordinator.roku.app_icon_url(self.app_id) @property - def app_name(self) -> str: + def app_name(self) -> str | None: """Name of the current running app.""" if self.coordinator.data.app is not None: return self.coordinator.data.app.name @@ -165,7 +164,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def app_id(self) -> str: + def app_id(self) -> str | None: """Return the ID of the current running app.""" if self.coordinator.data.app is not None: return self.coordinator.data.app.app_id @@ -173,7 +172,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def media_channel(self): + def media_channel(self) -> str | None: """Return the TV channel currently tuned.""" if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None: return None @@ -184,7 +183,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return self.coordinator.data.channel.number @property - def media_title(self): + def media_title(self) -> str | None: """Return the title of current playing media.""" if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None: return None @@ -195,7 +194,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" if self._media_playback_trackable(): return self.coordinator.data.media.duration @@ -203,7 +202,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" if self._media_playback_trackable(): return self.coordinator.data.media.position @@ -211,7 +210,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> dt.datetime | None: """When was the position of the current playing media valid.""" if self._media_playback_trackable(): return self.coordinator.data.media.at @@ -219,7 +218,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def source(self) -> str: + def source(self) -> str | None: """Return the current input source.""" if self.coordinator.data.app is not None: return self.coordinator.data.app.name @@ -237,8 +236,11 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): await self.coordinator.roku.search(keyword) async def async_get_browse_image( - self, media_content_type, media_content_id, media_image_id=None - ): + self, + media_content_type: str, + media_content_id: str, + media_image_id: str | None = None, + ) -> tuple[str | None, str | None]: """Fetch media browser image to serve via proxy.""" if media_content_type == MEDIA_TYPE_APP and media_content_id: image_url = self.coordinator.roku.app_icon_url(media_content_id) @@ -246,7 +248,11 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return (None, None) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" is_internal = is_internal_request(self.hass) diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 28095311d81..8f0d39ed1d9 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -6,8 +6,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RokuDataUpdateCoordinator, roku_exception_handler +from . import roku_exception_handler from .const import DOMAIN +from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity @@ -15,7 +16,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -) -> bool: +) -> None: """Load Roku remote based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number diff --git a/mypy.ini b/mypy.ini index 7e2b6f24632..eca6f699022 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1465,9 +1465,6 @@ ignore_errors = true [mypy-homeassistant.components.ring.*] ignore_errors = true -[mypy-homeassistant.components.roku.*] -ignore_errors = true - [mypy-homeassistant.components.rpi_power.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d5c28b0cc3c..b09fbbe98a9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -165,7 +165,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.recorder.*", "homeassistant.components.reddit.*", "homeassistant.components.ring.*", - "homeassistant.components.roku.*", "homeassistant.components.rpi_power.*", "homeassistant.components.ruckus_unleashed.*", "homeassistant.components.sabnzbd.*", diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 0964343e453..eb0d0028417 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -136,7 +136,7 @@ async def test_availability( await setup_integration(hass, aioclient_mock) with patch( - "homeassistant.components.roku.Roku.update", side_effect=RokuError + "homeassistant.components.roku.coordinator.Roku.update", side_effect=RokuError ), patch("homeassistant.util.dt.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -336,21 +336,21 @@ async def test_services( """Test the different media player services.""" await setup_integration(hass, aioclient_mock) - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, blocking=True ) remote_mock.assert_called_once_with("poweroff") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, blocking=True ) remote_mock.assert_called_once_with("poweron") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PAUSE, @@ -360,7 +360,7 @@ async def test_services( remote_mock.assert_called_once_with("play") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PLAY, @@ -370,7 +370,7 @@ async def test_services( remote_mock.assert_called_once_with("play") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, @@ -380,7 +380,7 @@ async def test_services( remote_mock.assert_called_once_with("play") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, @@ -390,7 +390,7 @@ async def test_services( remote_mock.assert_called_once_with("forward") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, @@ -400,7 +400,7 @@ async def test_services( remote_mock.assert_called_once_with("reverse") - with patch("homeassistant.components.roku.Roku.launch") as launch_mock: + with patch("homeassistant.components.roku.coordinator.Roku.launch") as launch_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -414,7 +414,7 @@ async def test_services( launch_mock.assert_called_once_with("11") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -424,7 +424,7 @@ async def test_services( remote_mock.assert_called_once_with("home") - with patch("homeassistant.components.roku.Roku.launch") as launch_mock: + with patch("homeassistant.components.roku.coordinator.Roku.launch") as launch_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -434,7 +434,7 @@ async def test_services( launch_mock.assert_called_once_with("12") - with patch("homeassistant.components.roku.Roku.launch") as launch_mock: + with patch("homeassistant.components.roku.coordinator.Roku.launch") as launch_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -458,14 +458,14 @@ async def test_tv_services( unique_id=TV_SERIAL, ) - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: TV_ENTITY_ID}, blocking=True ) remote_mock.assert_called_once_with("volume_up") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_DOWN, @@ -475,7 +475,7 @@ async def test_tv_services( remote_mock.assert_called_once_with("volume_down") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_MUTE, @@ -485,7 +485,7 @@ async def test_tv_services( remote_mock.assert_called_once_with("volume_mute") - with patch("homeassistant.components.roku.Roku.tune") as tune_mock: + with patch("homeassistant.components.roku.coordinator.Roku.tune") as tune_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -694,7 +694,7 @@ async def test_integration_services( """Test integration services.""" await setup_integration(hass, aioclient_mock) - with patch("homeassistant.components.roku.Roku.search") as search_mock: + with patch("homeassistant.components.roku.coordinator.Roku.search") as search_mock: await hass.services.async_call( DOMAIN, SERVICE_SEARCH, diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index 5b1c0509e1f..c0df380c1e8 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -42,7 +42,7 @@ async def test_main_services( """Test platform services.""" await setup_integration(hass, aioclient_mock) - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( REMOTE_DOMAIN, SERVICE_TURN_OFF, @@ -51,7 +51,7 @@ async def test_main_services( ) remote_mock.assert_called_once_with("poweroff") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( REMOTE_DOMAIN, SERVICE_TURN_ON, @@ -60,7 +60,7 @@ async def test_main_services( ) remote_mock.assert_called_once_with("poweron") - with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + with patch("homeassistant.components.roku.coordinator.Roku.remote") as remote_mock: await hass.services.async_call( REMOTE_DOMAIN, SERVICE_SEND_COMMAND, From 5321151799412bff3f0f5af48127d41bc069af68 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 04:31:11 -0400 Subject: [PATCH 081/818] Use entity class attributes for abode (#52427) * Use entity class attributes for abode * Apply suggestions from code review Co-authored-by: Franck Nijhof * move from base class * fix * Undo light supported features Co-authored-by: Franck Nijhof --- homeassistant/components/abode/__init__.py | 39 +++++-------------- .../components/abode/alarm_control_panel.py | 17 ++------ homeassistant/components/abode/sensor.py | 36 +++++------------ homeassistant/components/abode/switch.py | 7 +--- 4 files changed, 23 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 4e7709f9bf7..be474661eac 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -251,17 +251,13 @@ class AbodeEntity(Entity): """Initialize Abode entity.""" self._data = data self._available = True + self._attr_should_poll = data.polling @property def available(self): """Return the available state.""" return self._available - @property - def should_poll(self): - """Return the polling state.""" - return self._data.polling - async def async_added_to_hass(self): """Subscribe to Abode connection status updates.""" await self.hass.async_add_executor_job( @@ -291,6 +287,8 @@ class AbodeDevice(AbodeEntity): """Initialize Abode device.""" super().__init__(data) self._device = device + self._attr_name = device.name + self._attr_unique_id = device.device_uuid async def async_added_to_hass(self): """Subscribe to device events.""" @@ -312,11 +310,6 @@ class AbodeDevice(AbodeEntity): """Update device state.""" self._device.refresh() - @property - def name(self): - """Return the name of the device.""" - return self._device.name - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -328,11 +321,6 @@ class AbodeDevice(AbodeEntity): "device_type": self._device.type, } - @property - def unique_id(self): - """Return a unique ID to use for this device.""" - return self._device.device_uuid - @property def device_info(self): """Return device registry information for this entity.""" @@ -355,22 +343,13 @@ class AbodeAutomation(AbodeEntity): """Initialize for Abode automation.""" super().__init__(data) self._automation = automation + self._attr_name = automation.name + self._attr_unique_id = automation.automation_id + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + "type": "CUE automation", + } def update(self): """Update automation state.""" self._automation.refresh() - - @property - def name(self): - """Return the name of the automation.""" - return self._automation.name - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"} - - @property - def unique_id(self): - """Return a unique ID to use for this automation.""" - return self._automation.automation_id diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 6d0c030e3e1..0cc1b500ff4 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -28,10 +28,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): """An alarm_control_panel implementation for Abode.""" - @property - def icon(self): - """Return the icon.""" - return ICON + _attr_icon = ICON + _attr_code_arm_required = False + _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY @property def state(self): @@ -46,16 +45,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): state = None return state - @property - def code_arm_required(self): - """Whether the code is required for arm actions.""" - return False - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - def alarm_disarm(self, code=None): """Send disarm command.""" self._device.set_standby() diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index e3ececb62d9..f1f744a5511 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -41,23 +41,15 @@ class AbodeSensor(AbodeDevice, SensorEntity): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self._sensor_type = sensor_type - self._name = f"{self._device.name} {SENSOR_TYPES[self._sensor_type][0]}" - self._device_class = SENSOR_TYPES[self._sensor_type][1] - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - @property - def unique_id(self): - """Return a unique ID to use for this device.""" - return f"{self._device.device_uuid}-{self._sensor_type}" + self._attr_name = f"{device.name} {SENSOR_TYPES[sensor_type][0]}" + self._attr_device_class = SENSOR_TYPES[self._sensor_type][1] + self._attr_unique_id = f"{device.device_uuid}-{sensor_type}" + if self._sensor_type == CONST.TEMP_STATUS_KEY: + self._attr_unit_of_measurement = device.temp_unit + elif self._sensor_type == CONST.HUMI_STATUS_KEY: + self._attr_unit_of_measurement = device.humidity_unit + elif self._sensor_type == CONST.LUX_STATUS_KEY: + self._attr_unit_of_measurement = device.lux_unit @property def state(self): @@ -68,13 +60,3 @@ class AbodeSensor(AbodeDevice, SensorEntity): return self._device.humidity if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - if self._sensor_type == CONST.TEMP_STATUS_KEY: - return self._device.temp_unit - if self._sensor_type == CONST.HUMI_STATUS_KEY: - return self._device.humidity_unit - if self._sensor_type == CONST.LUX_STATUS_KEY: - return self._device.lux_unit diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 0985ce5ce2a..75c13962c43 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -48,6 +48,8 @@ class AbodeSwitch(AbodeDevice, SwitchEntity): class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity): """A switch implementation for Abode automations.""" + _attr_icon = ICON + async def async_added_to_hass(self): """Set up trigger automation service.""" await super().async_added_to_hass() @@ -73,8 +75,3 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity): def is_on(self): """Return True if the automation is enabled.""" return self._automation.is_enabled - - @property - def icon(self): - """Return the robot icon to match Home Assistant automations.""" - return ICON From 79ee112490488bfc3ce1205344ff767e42ba1df2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Jul 2021 10:33:12 +0200 Subject: [PATCH 082/818] Enable basic type checking for mqtt (#52463) * Enable basic type checking for mqtt * Tweak --- homeassistant/components/mqtt/__init__.py | 67 ++++++++++++------- .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/debug_info.py | 8 ++- .../components/mqtt/device_trigger.py | 14 ++-- homeassistant/components/mqtt/discovery.py | 6 +- homeassistant/components/mqtt/mixins.py | 11 +-- homeassistant/components/mqtt/models.py | 3 +- homeassistant/components/mqtt/sensor.py | 3 +- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - 10 files changed, 68 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3a6fd068975..9883e7b6ec8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -9,7 +9,7 @@ import logging from operator import attrgetter import ssl import time -from typing import Any, Callable, Union +from typing import Any, Awaitable, Callable, Union, cast import uuid import attr @@ -73,7 +73,12 @@ from .const import ( PROTOCOL_311, ) from .discovery import LAST_DISCOVERY -from .models import Message, MessageCallbackType, PublishPayloadType +from .models import ( + AsyncMessageCallbackType, + Message, + MessageCallbackType, + PublishPayloadType, +) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -284,26 +289,36 @@ def async_publish_template( hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) -def wrap_msg_callback(msg_callback: MessageCallbackType) -> MessageCallbackType: +AsyncDeprecatedMessageCallbackType = Callable[ + [str, PublishPayloadType, int], Awaitable[None] +] +DeprecatedMessageCallbackType = Callable[[str, PublishPayloadType, int], None] + + +def wrap_msg_callback( + msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, +) -> AsyncMessageCallbackType | MessageCallbackType: """Wrap an MQTT message callback to support deprecated signature.""" # Check for partials to properly determine if coroutine function check_func = msg_callback while isinstance(check_func, partial): check_func = check_func.func - wrapper_func = None + wrapper_func: AsyncMessageCallbackType | MessageCallbackType if asyncio.iscoroutinefunction(check_func): @wraps(msg_callback) - async def async_wrapper(msg: Any) -> None: + async def async_wrapper(msg: Message) -> None: """Call with deprecated signature.""" - await msg_callback(msg.topic, msg.payload, msg.qos) + await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( + msg.topic, msg.payload, msg.qos + ) wrapper_func = async_wrapper else: @wraps(msg_callback) - def wrapper(msg: Any) -> None: + def wrapper(msg: Message) -> None: """Call with deprecated signature.""" msg_callback(msg.topic, msg.payload, msg.qos) @@ -315,7 +330,10 @@ def wrap_msg_callback(msg_callback: MessageCallbackType) -> MessageCallbackType: async def async_subscribe( hass: HomeAssistant, topic: str, - msg_callback: MessageCallbackType, + msg_callback: AsyncMessageCallbackType + | MessageCallbackType + | DeprecatedMessageCallbackType + | AsyncDeprecatedMessageCallbackType, qos: int = DEFAULT_QOS, encoding: str | None = "utf-8", ): @@ -334,12 +352,15 @@ async def async_subscribe( wrapped_msg_callback = msg_callback # If we have 3 parameters with no default value, wrap the callback if non_default == 3: + module = inspect.getmodule(msg_callback) _LOGGER.warning( "Signature of MQTT msg_callback '%s.%s' is deprecated", - inspect.getmodule(msg_callback).__name__, + module.__name__ if module else "", msg_callback.__name__, ) - wrapped_msg_callback = wrap_msg_callback(msg_callback) + wrapped_msg_callback = wrap_msg_callback( + cast(DeprecatedMessageCallbackType, msg_callback) + ) async_remove = await hass.data[DATA_MQTT].async_subscribe( topic, @@ -378,16 +399,12 @@ def subscribe( async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry -) -> bool: +) -> None: """Try to start the discovery of MQTT devices. This method is a coroutine. """ - success: bool = await discovery.async_start( - hass, conf[CONF_DISCOVERY_PREFIX], config_entry - ) - - return success + await discovery.async_start(hass, conf[CONF_DISCOVERY_PREFIX], config_entry) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -539,7 +556,7 @@ class Subscription: matcher: Any = attr.ib() job: HassJob = attr.ib() qos: int = attr.ib(default=0) - encoding: str = attr.ib(default="utf-8") + encoding: str | None = attr.ib(default="utf-8") class MQTT: @@ -566,7 +583,7 @@ class MQTT: self._mqttc: mqtt.Client = None self._paho_lock = asyncio.Lock() - self._pending_operations = {} + self._pending_operations: dict[str, asyncio.Event] = {} if self.hass.state == CoreState.running: self._ha_started.set() @@ -688,12 +705,12 @@ class MQTT: _raise_on_error(msg_info.rc) await self._wait_for_mid(msg_info.mid) - async def async_connect(self) -> str: + async def async_connect(self) -> None: """Connect to the host. Does not process messages yet.""" # pylint: disable=import-outside-toplevel import paho.mqtt.client as mqtt - result: int = None + result: int | None = None try: result = await self.hass.async_add_executor_job( self._mqttc.connect, @@ -770,7 +787,7 @@ class MQTT: This method is a coroutine. """ async with self._paho_lock: - result: int = None + result: int | None = None result, mid = await self.hass.async_add_executor_job( self._mqttc.unsubscribe, topic ) @@ -781,7 +798,7 @@ class MQTT: async def _async_perform_subscription(self, topic: str, qos: int) -> None: """Perform a paho-mqtt subscription.""" async with self._paho_lock: - result: int = None + result: int | None = None result, mid = await self.hass.async_add_executor_job( self._mqttc.subscribe, topic, qos ) @@ -952,12 +969,12 @@ class MQTT: ) -def _raise_on_error(result_code: int) -> None: +def _raise_on_error(result_code: int | None) -> None: """Raise error if error result.""" # pylint: disable=import-outside-toplevel import paho.mqtt.client as mqtt - if result_code != 0: + if result_code is not None and result_code != 0: raise HomeAssistantError( f"Error talking to MQTT: {mqtt.error_string(result_code)}" ) @@ -1014,13 +1031,13 @@ async def websocket_remove_device(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "mqtt/subscribe", vol.Required("topic"): valid_subscribe_topic, } ) +@websocket_api.async_response async def websocket_subscribe(hass, connection, msg): """Subscribe to a MQTT topic.""" if not connection.user.is_admin: diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index e24abc27028..66dea3e3aa0 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -226,6 +226,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): def available(self) -> bool: """Return true if the device is available and value has not expired.""" expire_after = self._config.get(CONF_EXPIRE_AFTER) - return MqttAvailability.available.fget(self) and ( + # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 + return MqttAvailability.available.fget(self) and ( # type: ignore[attr-defined] expire_after is None or not self._expired ) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index d00d65c2451..57cb88e65e3 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -1,7 +1,7 @@ """Helper to handle a set of topics to subscribe to.""" from collections import deque from functools import wraps -from typing import Any +from typing import Any, Callable from homeassistant.core import HomeAssistant @@ -12,7 +12,9 @@ DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 -def log_messages(hass: HomeAssistant, entity_id: str) -> MessageCallbackType: +def log_messages( + hass: HomeAssistant, entity_id: str +) -> Callable[[MessageCallbackType], MessageCallbackType]: """Wrap an MQTT message callback to support message logging.""" def _log_message(msg): @@ -24,7 +26,7 @@ def log_messages(hass: HomeAssistant, entity_id: str) -> MessageCallbackType: if msg not in messages: messages.append(msg) - def _decorator(msg_callback: MessageCallbackType): + def _decorator(msg_callback: MessageCallbackType) -> MessageCallbackType: @wraps(msg_callback) def wrapper(msg: Any) -> None: """Log message.""" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index d9413b80c06..89246406de3 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -119,15 +119,15 @@ class Trigger: """Device trigger settings.""" device_id: str = attr.ib() - discovery_data: dict = attr.ib() + discovery_data: dict | None = attr.ib() hass: HomeAssistant = attr.ib() - payload: str = attr.ib() - qos: int = attr.ib() - remove_signal: Callable[[], None] = attr.ib() + payload: str | None = attr.ib() + qos: int | None = attr.ib() + remove_signal: Callable[[], None] | None = attr.ib() subtype: str = attr.ib() - topic: str = attr.ib() + topic: str | None = attr.ib() type: str = attr.ib() - value_template: str = attr.ib() + value_template: str | None = attr.ib() trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger(self, action, automation_info): @@ -289,7 +289,7 @@ async def async_device_removed(hass: HomeAssistant, device_id: str): async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for MQTT devices.""" - triggers = [] + triggers: list[dict] = [] if DEVICE_TRIGGERS not in hass.data: return triggers diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d35065e30a8..e0d1d0eb4dd 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -83,7 +83,7 @@ class MQTTConfig(dict): async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None -) -> bool: +) -> None: """Start MQTT Discovery.""" mqtt_integrations = {} @@ -298,10 +298,8 @@ async def async_start( # noqa: C901 0, ) - return True - -async def async_stop(hass: HomeAssistant) -> bool: +async def async_stop(hass: HomeAssistant) -> None: """Stop MQTT Discovery.""" if DISCOVERY_UNSUBSCRIBE in hass.data: for unsub in hass.data[DISCOVERY_UNSUBSCRIBE]: diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index b0c8b573b37..9e45d6d4f27 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import abstractmethod import json import logging +from typing import Callable import voluptuous as vol @@ -194,11 +195,11 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): class MqttAttributes(Entity): """Mixin used for platforms that support JSON attributes.""" - _attributes_extra_blocked = frozenset() + _attributes_extra_blocked: frozenset[str] = frozenset() def __init__(self, config: dict) -> None: """Initialize the JSON attributes mixin.""" - self._attributes = None + self._attributes: dict | None = None self._attributes_sub_state = None self._attributes_config = config @@ -225,7 +226,7 @@ class MqttAttributes(Entity): payload = msg.payload if attr_tpl is not None: payload = attr_tpl.async_render_with_possible_json_value(payload) - json_dict = json.loads(payload) + json_dict = json.loads(payload) if isinstance(payload, str) else None if isinstance(json_dict, dict): filtered_dict = { k: v @@ -272,7 +273,7 @@ class MqttAvailability(Entity): def __init__(self, config: dict) -> None: """Initialize the availability mixin.""" self._availability_sub_state = None - self._available = {} + self._available: dict = {} self._available_latest = False self._availability_setup_from_config(config) @@ -397,7 +398,7 @@ class MqttDiscoveryUpdate(Entity): """Initialize the discovery update mixin.""" self._discovery_data = discovery_data self._discovery_update = discovery_update - self._remove_signal = None + self._remove_signal: Callable | None = None self._removed_from_hass = False async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 7cdafeef98d..0c8c311d768 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -2,7 +2,7 @@ from __future__ import annotations import datetime as dt -from typing import Callable, Union +from typing import Awaitable, Callable, Union import attr @@ -21,4 +21,5 @@ class Message: timestamp: dt.datetime | None = attr.ib(default=None) +AsyncMessageCallbackType = Callable[[Message], Awaitable[None]] MessageCallbackType = Callable[[Message], None] diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 777a15b639a..0c234fbbbea 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -242,6 +242,7 @@ class MqttSensor(MqttEntity, SensorEntity): def available(self) -> bool: """Return true if the device is available and value has not expired.""" expire_after = self._config.get(CONF_EXPIRE_AFTER) - return MqttAvailability.available.fget(self) and ( + # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 + return MqttAvailability.available.fget(self) and ( # type: ignore[attr-defined] expire_after is None or not self._expired ) diff --git a/mypy.ini b/mypy.ini index eca6f699022..5ed805a1b59 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1345,9 +1345,6 @@ ignore_errors = true [mypy-homeassistant.components.motion_blinds.*] ignore_errors = true -[mypy-homeassistant.components.mqtt.*] -ignore_errors = true - [mypy-homeassistant.components.mullvad.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b09fbbe98a9..8b9b15d35aa 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -125,7 +125,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server.*", "homeassistant.components.mobile_app.*", "homeassistant.components.motion_blinds.*", - "homeassistant.components.mqtt.*", "homeassistant.components.mullvad.*", "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", From 6d9628423ce8359050db7272e80c95942ffbaaf9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 04:37:00 -0400 Subject: [PATCH 083/818] Use entity class attributes for aftership (#52500) * Use entity class attributes for aftership * Tweaks Co-authored-by: Franck Nijhof --- homeassistant/components/aftership/sensor.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 4d3fb17b949..fd8e095f65f 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -109,38 +109,26 @@ async def async_setup_platform( class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" + _attr_unit_of_measurement: str = "packages" + _attr_icon: str = ICON + def __init__(self, aftership: Tracking, name: str) -> None: """Initialize the sensor.""" self._attributes: dict[str, Any] = {} - self._name: str = name self._state: int | None = None self.aftership = aftership - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name + self._attr_name = name @property def state(self) -> int | None: """Return the state of the sensor.""" return self._state - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return "packages" - @property def extra_state_attributes(self) -> dict[str, str]: """Return attributes for the sensor.""" return self._attributes - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return ICON - async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( From 6fc5a6a065bf54fe4f90b5491c3513f3dc06b041 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 5 Jul 2021 11:38:55 +0300 Subject: [PATCH 084/818] Fix CI failing due to Shell Command exception (#52483) --- homeassistant/components/shell_command/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index dc0deef1b82..48744b4fea2 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -92,6 +92,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if process: with suppress(TypeError): process.kill() + # https://bugs.python.org/issue43884 + # pylint: disable=protected-access + process._transport.close() # type: ignore[attr-defined] del process return From 05b35cd98a722d415ba2ca765d0009192d673a7d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 05:05:53 -0400 Subject: [PATCH 085/818] Rename goalzero sensor (#52452) --- homeassistant/components/goalzero/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index 643c632a352..327a4f4833c 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -129,7 +129,7 @@ SENSOR_DICT = { ATTR_DEFAULT_ENABLED: True, }, "timestamp": { - ATTR_NAME: "Up Time", + ATTR_NAME: "Total Run Time", ATTR_UNIT_OF_MEASUREMENT: TIME_SECONDS, ATTR_DEFAULT_ENABLED: False, }, From 600bea24592f01c06bd683ddae382f8af279ae4b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Jul 2021 11:14:41 +0200 Subject: [PATCH 086/818] Enable basic type checking for Google cast (#52434) * Enable basic type checking for Google cast * tweak --- homeassistant/components/cast/helpers.py | 2 +- homeassistant/components/cast/home_assistant_cast.py | 3 ++- homeassistant/components/cast/media_player.py | 8 ++++---- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 71caa6490d8..6d021d020c4 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -43,7 +43,7 @@ class ChromecastInfo: ) @property - def manufacturer(self) -> str: + def manufacturer(self) -> str | None: """Return the manufacturer.""" if self._manufacturer: return self._manufacturer diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index bb0354bb68e..fb2d790d03d 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -82,4 +82,5 @@ async def async_remove_user( if user_id is not None: user = await hass.auth.async_get_user(user_id) - await hass.auth.async_remove_user(user) + if user: + await hass.auth.async_remove_user(user) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 07e97dd1a7e..fde9b23704d 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from contextlib import suppress -from datetime import timedelta +from datetime import datetime, timedelta import functools as ft import json import logging @@ -169,8 +169,8 @@ class CastDevice(MediaPlayerEntity): self.cast_status = None self.media_status = None self.media_status_received = None - self.mz_media_status = {} - self.mz_media_status_received = {} + self.mz_media_status: dict[str, pychromecast.controllers.media.MediaStatus] = {} + self.mz_media_status_received: dict[str, datetime] = {} self.mz_mgr = None self._available = False self._status_listener: CastStatusListener | None = None @@ -774,7 +774,7 @@ class CastDevice(MediaPlayerEntity): url_path: str | None, ): """Handle a show view signal.""" - if entity_id != self.entity_id: + if entity_id != self.entity_id or self._chromecast is None: return if self._hass_cast_controller is None: diff --git a/mypy.ini b/mypy.ini index 5ed805a1b59..c3dd71d8229 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1060,9 +1060,6 @@ ignore_errors = true [mypy-homeassistant.components.bsblan.*] ignore_errors = true -[mypy-homeassistant.components.cast.*] -ignore_errors = true - [mypy-homeassistant.components.cert_expiry.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 8b9b15d35aa..dbde9516f8c 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -30,7 +30,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.blueprint.*", "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bsblan.*", - "homeassistant.components.cast.*", "homeassistant.components.cert_expiry.*", "homeassistant.components.climacell.*", "homeassistant.components.cloud.*", From 1cc8280959eefaa8139fc6a6fdfa0dc3f0847917 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Jul 2021 11:26:31 +0200 Subject: [PATCH 087/818] Enable basic type checking for the homeassistant component (#52464) * Enable basic type checking for the homeassistant component * Tweak --- .../components/homeassistant/scene.py | 18 +++++++++++++----- .../homeassistant/triggers/numeric_state.py | 2 +- .../components/homeassistant/triggers/state.py | 5 +++-- .../components/homeassistant/triggers/time.py | 2 +- homeassistant/helpers/event.py | 4 +++- mypy.ini | 18 ------------------ script/hassfest/mypy_config.py | 6 ------ 7 files changed, 21 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 13a4ef66383..9ae271baa72 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,9 +1,8 @@ """Allow users to set and activate scenes.""" from __future__ import annotations -from collections import namedtuple import logging -from typing import Any +from typing import Any, NamedTuple import voluptuous as vol @@ -115,10 +114,19 @@ CREATE_SCENE_SCHEMA = vol.All( SERVICE_APPLY = "apply" SERVICE_CREATE = "create" -SCENECONFIG = namedtuple("SceneConfig", [CONF_ID, CONF_NAME, CONF_ICON, STATES]) + _LOGGER = logging.getLogger(__name__) +class SceneConfig(NamedTuple): + """Object for storing scene config.""" + + id: str + name: str + icon: str + states: dict + + @callback def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all scenes that reference the entity.""" @@ -238,7 +246,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.warning("Empty scenes are not allowed") return - scene_config = SCENECONFIG(None, call.data[CONF_SCENE_ID], None, entities) + scene_config = SceneConfig(None, call.data[CONF_SCENE_ID], None, entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" old = platform.entities.get(entity_id) if old is not None: @@ -264,7 +272,7 @@ def _process_scenes_config(hass, async_add_entities, config): async_add_entities( HomeAssistantScene( hass, - SCENECONFIG( + SceneConfig( scene.get(CONF_ID), scene[CONF_NAME], scene.get(CONF_ICON), diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 366f937a192..f315addb272 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -79,7 +79,7 @@ async def async_attach_trigger( job = HassJob(action) trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} - _variables = {} + _variables: dict = {} if automation_info: _variables = automation_info.get("variables") or {} diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 2c96b6be944..12c42a95978 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -88,7 +88,7 @@ async def async_attach_trigger( job = HassJob(action) trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} - _variables = {} + _variables: dict = {} if automation_info: _variables = automation_info.get("variables") or {} @@ -171,10 +171,11 @@ async def async_attach_trigger( ) return - def _check_same_state(_, _2, new_st: State): + def _check_same_state(_, _2, new_st: State | None) -> bool: if new_st is None: return False + cur_value: str | None if attribute is None: cur_value = new_st.state else: diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index ff78e4c43c8..f661ae21a5b 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util _TIME_TRIGGER_SCHEMA = vol.Any( cv.time, - vol.All(str, cv.entity_domain(("input_datetime", "sensor"))), + vol.All(str, cv.entity_domain(["input_datetime", "sensor"])), msg="Expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'", ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 85eebf05298..1fe542c096c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1440,7 +1440,9 @@ def async_track_time_change( track_time_change = threaded_listener_factory(async_track_time_change) -def process_state_match(parameter: None | str | Iterable[str]) -> Callable[[str], bool]: +def process_state_match( + parameter: None | str | Iterable[str], +) -> Callable[[str | None], bool]: """Convert parameter to function that matches input against parameter.""" if parameter is None or parameter == MATCH_ALL: return lambda _: True diff --git a/mypy.ini b/mypy.ini index c3dd71d8229..f6079c74e13 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1210,24 +1210,6 @@ ignore_errors = true [mypy-homeassistant.components.home_plus_control.*] ignore_errors = true -[mypy-homeassistant.components.homeassistant.triggers.homeassistant] -ignore_errors = true - -[mypy-homeassistant.components.homeassistant.triggers.numeric_state] -ignore_errors = true - -[mypy-homeassistant.components.homeassistant.triggers.time_pattern] -ignore_errors = true - -[mypy-homeassistant.components.homeassistant.triggers.time] -ignore_errors = true - -[mypy-homeassistant.components.homeassistant.triggers.state] -ignore_errors = true - -[mypy-homeassistant.components.homeassistant.scene] -ignore_errors = true - [mypy-homeassistant.components.homekit.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index dbde9516f8c..d234f96589a 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -80,12 +80,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.hisense_aehw4a1.*", "homeassistant.components.home_connect.*", "homeassistant.components.home_plus_control.*", - "homeassistant.components.homeassistant.triggers.homeassistant", - "homeassistant.components.homeassistant.triggers.numeric_state", - "homeassistant.components.homeassistant.triggers.time_pattern", - "homeassistant.components.homeassistant.triggers.time", - "homeassistant.components.homeassistant.triggers.state", - "homeassistant.components.homeassistant.scene", "homeassistant.components.homekit.*", "homeassistant.components.homekit_controller.*", "homeassistant.components.homematicip_cloud.*", From 5367a036d22b4f6ba91182dd79cbb7fa6ee97f74 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 05:36:04 -0400 Subject: [PATCH 088/818] Use entity class attributes for accuweather (#52431) --- .../components/accuweather/sensor.py | 60 +++++-------------- .../components/accuweather/weather.py | 38 +++--------- 2 files changed, 25 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index ba99df14d9e..3a774afe341 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -13,7 +13,6 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -84,32 +83,27 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): else: self._description = FORECAST_SENSOR_TYPES[kind] self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - self._name = name self.kind = kind - self._device_class = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self.forecast_day = forecast_day self._attr_state_class = self._description.get(ATTR_STATE_CLASS) - - @property - def name(self) -> str: - """Return the name.""" + self._attr_icon = self._description[ATTR_ICON] + self._attr_device_class = self._description[ATTR_DEVICE_CLASS] + self._attr_entity_registry_enabled_default = self._description[ATTR_ENABLED] if self.forecast_day is not None: - return f"{self._name} {self._description[ATTR_LABEL]} {self.forecast_day}d" - return f"{self._name} {self._description[ATTR_LABEL]}" - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - if self.forecast_day is not None: - return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower() - return f"{self.coordinator.location_key}-{self.kind}".lower() - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.coordinator.location_key)}, + self._attr_name = f"{name} {self._description[ATTR_LABEL]} {forecast_day}d" + self._attr_unique_id = ( + f"{coordinator.location_key}-{kind}-{forecast_day}".lower() + ) + else: + self._attr_name = f"{name} {self._description[ATTR_LABEL]}" + self._attr_unique_id = f"{coordinator.location_key}-{kind}".lower() + if coordinator.is_metric: + self._attr_unit_of_measurement = self._description[ATTR_UNIT_METRIC] + else: + self._attr_unit_of_measurement = self._description[ATTR_UNIT_IMPERIAL] + self._attr_device_info = { + "identifiers": {(DOMAIN, coordinator.location_key)}, "name": NAME, "manufacturer": MANUFACTURER, "entry_type": "service", @@ -139,23 +133,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): return cast(StateType, self._sensor_data["Speed"]["Value"]) return cast(StateType, self._sensor_data) - @property - def icon(self) -> str | None: - """Return the icon.""" - return self._description[ATTR_ICON] - - @property - def device_class(self) -> str | None: - """Return the device_class.""" - return self._description[ATTR_DEVICE_CLASS] - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit the value is expressed in.""" - if self.coordinator.is_metric: - return self._description[ATTR_UNIT_METRIC] - return self._description[ATTR_UNIT_IMPERIAL] - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" @@ -171,11 +148,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self._attrs["type"] = self.coordinator.data["PrecipitationType"] return self._attrs - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._description[ATTR_ENABLED] - @callback def _handle_coordinator_update(self) -> None: """Handle data update.""" diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 9a2ba769a82..cd8d64cc80f 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -19,7 +19,6 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp @@ -60,29 +59,15 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._name = name - self._unit_system = API_METRIC if self.coordinator.is_metric else API_IMPERIAL - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def attribution(self) -> str: - """Return the attribution.""" - return ATTRIBUTION - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return self.coordinator.location_key - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.coordinator.location_key)}, + self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL + self._attr_name = name + self._attr_unique_id = coordinator.location_key + self._attr_temperature_unit = ( + TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT + ) + self._attr_attribution = ATTRIBUTION + self._attr_device_info = { + "identifiers": {(DOMAIN, coordinator.location_key)}, "name": NAME, "manufacturer": MANUFACTURER, "entry_type": "service", @@ -107,11 +92,6 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): float, self.coordinator.data["Temperature"][self._unit_system]["Value"] ) - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT - @property def pressure(self) -> float: """Return the pressure.""" From 64e63dedf67b1095e465578226c69d6e0a5c192c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 5 Jul 2021 04:38:31 -0500 Subject: [PATCH 089/818] Revert "Force SimpliSafe to reauthenticate with a password (#51528)" (#52484) This reverts commit 549f779b0679b004f67aca996b107414355a6e36. --- .../components/simplisafe/__init__.py | 73 ++++++++++++------- .../components/simplisafe/config_flow.py | 13 ++-- .../components/simplisafe/strings.json | 2 +- .../simplisafe/translations/en.json | 2 +- .../components/simplisafe/test_config_flow.py | 20 ++--- 5 files changed, 57 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index fce1890b280..c4856788848 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -6,7 +6,7 @@ from simplipy import API from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -105,6 +105,14 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) +@callback +def _async_save_refresh_token(hass, config_entry, token): + """Save a refresh token to the config entry.""" + hass.config_entries.async_update_entry( + config_entry, data={**config_entry.data, CONF_TOKEN: token} + ) + + async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -131,9 +139,6 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] - if CONF_PASSWORD not in config_entry.data: - raise ConfigEntryAuthFailed("Config schema change requires re-authentication") - entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -155,24 +160,20 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) - async def async_get_api(): - """Define a helper to get an authenticated SimpliSafe API object.""" - return await API.login_via_credentials( - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], - client_id=client_id, - session=websession, - ) - try: - api = await async_get_api() - except InvalidCredentialsError as err: - raise ConfigEntryAuthFailed from err + api = await API.login_via_token( + config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + ) + except InvalidCredentialsError: + LOGGER.error("Invalid credentials provided") + return False except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - simplisafe = SimpliSafe(hass, config_entry, api, async_get_api) + _async_save_refresh_token(hass, config_entry, api.refresh_token) + + simplisafe = SimpliSafe(hass, api, config_entry) try: await simplisafe.async_init() @@ -295,10 +296,10 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, config_entry, api, async_get_api): + def __init__(self, hass, api, config_entry): """Initialize.""" self._api = api - self._async_get_api = async_get_api + self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -375,17 +376,23 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - try: - self._api = await self._async_get_api() - return - except InvalidCredentialsError as err: + if self._emergency_refresh_token_used: raise ConfigEntryAuthFailed( - "Unable to re-authenticate with SimpliSafe" - ) from err + "Update failed with stored refresh token" + ) + + LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") + self._emergency_refresh_token_used = True + + try: + await self._api.refresh_access_token( + self.config_entry.data[CONF_TOKEN] + ) + return except SimplipyError as err: - raise UpdateFailed( - f"SimpliSafe error while updating: {err}" - ) from err + raise UpdateFailed( # pylint: disable=raise-missing-from + f"Error while using stored refresh token: {err}" + ) if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -396,6 +403,16 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") + if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: + _async_save_refresh_token( + self._hass, self.config_entry, self._api.refresh_token + ) + + # If we've reached this point using an emergency refresh token, we're in the + # clear and we can discard it: + if self._emergency_refresh_token_used: + self._emergency_refresh_token_used = False + class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0faa07221aa..ba51356f770 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - await self._async_get_simplisafe_api() + simplisafe = await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_TOKEN: simplisafe.refresh_token, CONF_CODE: self._code, } ) @@ -89,9 +89,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -101,7 +98,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - await self._async_get_simplisafe_api() + simplisafe = await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -111,7 +108,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_TOKEN: simplisafe.refresh_token, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 23f85495025..ad973261a0e 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 331eb65ca83..b9e274666bb 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index c2397e9f89e..a048e4b0745 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,7 +10,7 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from tests.common import MockConfigEntry @@ -33,11 +33,7 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - }, + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -106,11 +102,7 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - }, + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -128,8 +120,6 @@ async def test_step_reauth(hass): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) - ), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -161,7 +151,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_TOKEN: "12345abc", CONF_CODE: "1234", } @@ -207,7 +197,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_TOKEN: "12345abc", CONF_CODE: "1234", } From 2e4f5135264cd801f42a19ffd0b20918062f008e Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Mon, 5 Jul 2021 11:40:13 +0200 Subject: [PATCH 090/818] Add type annotations and shorten sensor names on ezviz sensor platforms (#52475) * Add basic typing and change name on sensor platforms. * Complete type annotations for all functions. --- .../components/ezviz/binary_sensor.py | 35 ++++++++++++----- homeassistant/components/ezviz/sensor.py | 38 ++++++++++++++----- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index abfe06d8daf..bc343f06065 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -4,16 +4,25 @@ import logging from pyezviz.constants import BinarySensorType from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .coordinator import EzvizDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Ezviz sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] sensors = [] for idx, camera in enumerate(coordinator.data): @@ -34,7 +43,15 @@ async def async_setup_entry(hass, entry, async_add_entities): class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of a Ezviz sensor.""" - def __init__(self, coordinator, idx, name, sensor_type_name): + coordinator: EzvizDataUpdateCoordinator + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + idx: int, + name: str, + sensor_type_name: str, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._idx = idx @@ -45,22 +62,22 @@ class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): self._serial = self.coordinator.data[self._idx]["serial"] @property - def name(self): + def name(self) -> str: """Return the name of the Ezviz sensor.""" - return self._sensor_name + return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the sensor.""" return self.coordinator.data[self._idx][self._name] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this sensor.""" return f"{self._serial}_{self._sensor_name}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, self._serial)}, @@ -71,6 +88,6 @@ class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): } @property - def device_class(self): + def device_class(self) -> str: """Device class for the sensor.""" return self.sensor_type_name diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index fc07db89509..4e81ef6a6a7 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,19 +1,29 @@ """Support for Ezviz sensors.""" +from __future__ import annotations + import logging from pyezviz.constants import SensorType -from homeassistant.helpers.entity import Entity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .coordinator import EzvizDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Ezviz sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] sensors = [] for idx, camera in enumerate(coordinator.data): @@ -32,7 +42,15 @@ async def async_setup_entry(hass, entry, async_add_entities): class EzvizSensor(CoordinatorEntity, Entity): """Representation of a Ezviz sensor.""" - def __init__(self, coordinator, idx, name, sensor_type_name): + coordinator: EzvizDataUpdateCoordinator + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + idx: int, + name: str, + sensor_type_name: str, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._idx = idx @@ -43,22 +61,22 @@ class EzvizSensor(CoordinatorEntity, Entity): self._serial = self.coordinator.data[self._idx]["serial"] @property - def name(self): + def name(self) -> str: """Return the name of the Ezviz sensor.""" - return self._sensor_name + return self._name @property - def state(self): + def state(self) -> int | str: """Return the state of the sensor.""" return self.coordinator.data[self._idx][self._name] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this sensor.""" return f"{self._serial}_{self._sensor_name}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, self._serial)}, @@ -69,6 +87,6 @@ class EzvizSensor(CoordinatorEntity, Entity): } @property - def device_class(self): + def device_class(self) -> str: """Device class for the sensor.""" return self.sensor_type_name From 1cb298948f782deb809988d6f7b9b36fb5ad0efa Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 5 Jul 2021 11:45:50 +0200 Subject: [PATCH 091/818] Fix MODBUS connection type rtuovertcp does not connect (#52505) * Correct host -> framer. * Use function pointer --- homeassistant/components/modbus/modbus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 35572baff43..2e5892dbf1d 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -5,6 +5,7 @@ import logging from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.constants import Defaults from pymodbus.exceptions import ModbusException +from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( CONF_DELAY, @@ -224,7 +225,7 @@ class ModbusHub: # network configuration self._pb_params["host"] = client_config[CONF_HOST] if self._config_type == CONF_RTUOVERTCP: - self._pb_params["host"] = "ModbusRtuFramer" + self._pb_params["framer"] = ModbusRtuFramer Defaults.Timeout = client_config[CONF_TIMEOUT] From 323088ff6309939211ac85e562d65389716745c6 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Mon, 5 Jul 2021 12:54:00 +0300 Subject: [PATCH 092/818] Fully type Tag component (#52540) --- .strict-typing | 1 + homeassistant/components/tag/__init__.py | 11 +++++++---- homeassistant/components/tag/trigger.py | 13 ++++++++++--- mypy.ini | 11 +++++++++++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.strict-typing b/.strict-typing index f267925ad19..d60b99822b9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -78,6 +78,7 @@ homeassistant.components.sun.* homeassistant.components.switch.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* +homeassistant.components.tag.* homeassistant.components.tcp.* homeassistant.components.tts.* homeassistant.components.upcloud.* diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 4a410c6b30b..c05b4416343 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -7,11 +7,12 @@ import uuid import voluptuous as vol from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util @@ -75,7 +76,7 @@ class TagStorageCollection(collection.StorageCollection): return data @callback - def _get_suggested_id(self, info: dict) -> str: + def _get_suggested_id(self, info: dict[str, str]) -> str: """Suggest an ID based on the config.""" return info[TAG_ID] @@ -88,7 +89,7 @@ class TagStorageCollection(collection.StorageCollection): return data -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Tag component.""" hass.data[DOMAIN] = {} id_manager = TagIDManager() @@ -106,7 +107,9 @@ async def async_setup(hass: HomeAssistant, config: dict): @bind_hass -async def async_scan_tag(hass, tag_id, device_id, context=None): +async def async_scan_tag( + hass: HomeAssistant, tag_id: str, device_id: str, context: Context | None = None +) -> None: """Handle when a tag is scanned.""" if DOMAIN not in hass.config.components: raise HomeAssistantError("tag component has not been set up.") diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index 1984505f3a6..ba90f0a9396 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -1,9 +1,11 @@ """Support for tag triggers.""" import voluptuous as vol +from homeassistant.components.automation import AutomationActionType from homeassistant.const import CONF_PLATFORM -from homeassistant.core import HassJob +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID @@ -16,7 +18,12 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: """Listen for tag_scanned events based on configuration.""" trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} tag_ids = set(config[TAG_ID]) @@ -24,7 +31,7 @@ async def async_attach_trigger(hass, config, action, automation_info): job = HassJob(action) - async def handle_event(event): + async def handle_event(event: Event) -> None: """Listen for tag scan events and calls the action when data matches.""" if event.data.get(TAG_ID) not in tag_ids or ( device_ids is not None and event.data.get(DEVICE_ID) not in device_ids diff --git a/mypy.ini b/mypy.ini index f6079c74e13..521e5cc4b7d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -869,6 +869,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tag.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.tcp.*] check_untyped_defs = true disallow_incomplete_defs = true From 4cac85e3f5935270041504b6e10388770c88b8c4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Jul 2021 06:03:16 -0400 Subject: [PATCH 093/818] Use entity class attributes for ads (#52488) * Use entity class attributes for ads * fix * Update homeassistant/components/ads/cover.py Co-authored-by: Shay Levy Co-authored-by: Franck Nijhof Co-authored-by: Shay Levy --- homeassistant/components/ads/__init__.py | 23 ++++------------ homeassistant/components/ads/binary_sensor.py | 9 ++----- homeassistant/components/ads/cover.py | 27 +++++-------------- homeassistant/components/ads/light.py | 15 +++++------ homeassistant/components/ads/sensor.py | 10 +++---- homeassistant/components/ads/switch.py | 2 +- 6 files changed, 24 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index b17a066eba7..d59d1e5aa0c 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -268,15 +268,17 @@ class AdsHub: class AdsEntity(Entity): """Representation of ADS entity.""" + _attr_should_poll = False + def __init__(self, ads_hub, name, ads_var): """Initialize ADS binary sensor.""" - self._name = name - self._unique_id = ads_var self._state_dict = {} self._state_dict[STATE_KEY_STATE] = None self._ads_hub = ads_hub self._ads_var = ads_var self._event = None + self._attr_unique_id = ads_var + self._attr_name = name async def async_initialize_device( self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None @@ -311,21 +313,6 @@ class AdsEntity(Entity): _LOGGER.debug("Variable %s: Timeout during first update", ads_var) @property - def name(self): - """Return the default name of the binary sensor.""" - return self._name - - @property - def unique_id(self): - """Return an unique identifier for this entity.""" - return self._unique_id - - @property - def should_poll(self): - """Return False because entity pushes its state to HA.""" - return False - - @property - def available(self): + def available(self) -> bool: """Return False if state has not been updated yet.""" return self._state_dict[STATE_KEY_STATE] is not None diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 0cf89dfa7cc..fda2aae3d5b 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -40,18 +40,13 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity): def __init__(self, ads_hub, name, ads_var, device_class): """Initialize ADS binary sensor.""" super().__init__(ads_hub, name, ads_var) - self._device_class = device_class or DEVICE_CLASS_MOVING + self._attr_device_class = device_class or DEVICE_CLASS_MOVING async def async_added_to_hass(self): """Register device notification.""" await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL) @property - def is_on(self): + def is_on(self) -> bool: """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] - - @property - def device_class(self): - """Return the device class.""" - return self._device_class diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index 5348873c7d0..0cd0264cb50 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -105,7 +105,12 @@ class AdsCover(AdsEntity, CoverEntity): self._ads_var_open = ads_var_open self._ads_var_close = ads_var_close self._ads_var_stop = ads_var_stop - self._device_class = device_class + self._attr_device_class = device_class + self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + if ads_var_stop is not None: + self._attr_supported_features |= SUPPORT_STOP + if ads_var_pos_set is not None: + self._attr_supported_features |= SUPPORT_SET_POSITION async def async_added_to_hass(self): """Register device notification.""" @@ -119,11 +124,6 @@ class AdsCover(AdsEntity, CoverEntity): self._ads_var_position, self._ads_hub.PLCTYPE_BYTE, STATE_KEY_POSITION ) - @property - def device_class(self): - """Return the class of this cover.""" - return self._device_class - @property def is_closed(self): """Return if the cover is closed.""" @@ -138,19 +138,6 @@ class AdsCover(AdsEntity, CoverEntity): """Return current position of cover.""" return self._state_dict[STATE_KEY_POSITION] - @property - def supported_features(self): - """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE - - if self._ads_var_stop is not None: - supported_features |= SUPPORT_STOP - - if self._ads_var_pos_set is not None: - supported_features |= SUPPORT_SET_POSITION - - return supported_features - def stop_cover(self, **kwargs): """Fire the stop action.""" if self._ads_var_stop: @@ -185,7 +172,7 @@ class AdsCover(AdsEntity, CoverEntity): self.set_cover_position(position=0) @property - def available(self): + def available(self) -> bool: """Return False if state has not been updated yet.""" if self._ads_var is not None or self._ads_var_position is not None: return ( diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index 80ee5df0c4b..fd6b5e66482 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -1,4 +1,6 @@ """Support for ADS light sources.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.light import ( @@ -48,6 +50,8 @@ class AdsLight(AdsEntity, LightEntity): super().__init__(ads_hub, name, ads_var_enable) self._state_dict[STATE_KEY_BRIGHTNESS] = None self._ads_var_brightness = ads_var_brightness + if ads_var_brightness is not None: + self._attr_supported_features = SUPPORT_BRIGHTNESS async def async_added_to_hass(self): """Register device notification.""" @@ -61,19 +65,12 @@ class AdsLight(AdsEntity, LightEntity): ) @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of the light (0..255).""" return self._state_dict[STATE_KEY_BRIGHTNESS] @property - def supported_features(self): - """Flag supported features.""" - if self._ads_var_brightness is not None: - return SUPPORT_BRIGHTNESS - return 0 - - @property - def is_on(self): + def is_on(self) -> bool: """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 933950dcf1b..fe68c4c860b 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components import ads from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import StateType from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity @@ -49,7 +50,7 @@ class AdsSensor(AdsEntity, SensorEntity): def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): """Initialize AdsSensor entity.""" super().__init__(ads_hub, name, ads_var) - self._unit_of_measurement = unit_of_measurement + self._attr_unit_of_measurement = unit_of_measurement self._ads_type = ads_type self._factor = factor @@ -63,11 +64,6 @@ class AdsSensor(AdsEntity, SensorEntity): ) @property - def state(self): + def state(self) -> StateType: """Return the state of the device.""" return self._state_dict[STATE_KEY_STATE] - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 9f807899e54..4888c876e1d 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -35,7 +35,7 @@ class AdsSwitch(AdsEntity, SwitchEntity): await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL) @property - def is_on(self): + def is_on(self) -> bool: """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] From f9c7137d02a2e287d6f6ac0f50a140d104bb6de5 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 5 Jul 2021 13:05:18 +0200 Subject: [PATCH 094/818] Use dataclasses in netatmo data handler (#52537) * Switch to using dataclasses * Clean up * Update homeassistant/components/netatmo/data_handler.py --- .../components/netatmo/data_handler.py | 48 +++++++++++-------- .../components/netatmo/netatmo_entity_base.py | 2 +- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index e93e602d6a7..5e007e8634d 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections import deque +from dataclasses import dataclass from datetime import timedelta from itertools import islice import logging @@ -34,8 +35,6 @@ HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData" HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus" PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" -NEXT_SCAN = "next_scan" - DATA_CLASSES = { WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, @@ -57,6 +56,16 @@ DEFAULT_INTERVALS = { SCAN_INTERVAL = 60 +@dataclass +class NetatmoDataClass: + """Class for keeping track of Netatmo data class metadata.""" + + name: str + interval: int + next_scan: float + subscriptions: list[CALLBACK_TYPE] + + class NetatmoDataHandler: """Manages the Netatmo data handling.""" @@ -93,12 +102,12 @@ class NetatmoDataHandler: to minimize the calls on the api service. """ for data_class in islice(self._queue, 0, BATCH_SIZE): - if data_class[NEXT_SCAN] > time(): + if data_class.next_scan > time(): continue - if data_class_name := data_class["name"]: - self.data_classes[data_class_name][NEXT_SCAN] = ( - time() + data_class["interval"] + if data_class_name := data_class.name: + self.data_classes[data_class_name].next_scan = ( + time() + data_class.interval ) await self.async_fetch_data(data_class_name) @@ -108,7 +117,7 @@ class NetatmoDataHandler: @callback def async_force_update(self, data_class_entry): """Prioritize data retrieval for given data class entry.""" - self.data_classes[data_class_entry][NEXT_SCAN] = time() + self.data_classes[data_class_entry].next_scan = time() self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) async def async_cleanup(self): @@ -149,7 +158,7 @@ class NetatmoDataHandler: _LOGGER.debug(err) return - for update_callback in self.data_classes[data_class_entry]["subscriptions"]: + for update_callback in self.data_classes[data_class_entry].subscriptions: if update_callback: update_callback() @@ -158,21 +167,18 @@ class NetatmoDataHandler: ): """Register data class.""" if data_class_entry in self.data_classes: - if ( - update_callback - not in self.data_classes[data_class_entry]["subscriptions"] - ): - self.data_classes[data_class_entry]["subscriptions"].append( + if update_callback not in self.data_classes[data_class_entry].subscriptions: + self.data_classes[data_class_entry].subscriptions.append( update_callback ) return - self.data_classes[data_class_entry] = { - "name": data_class_entry, - "interval": DEFAULT_INTERVALS[data_class_name], - NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], - "subscriptions": [update_callback], - } + self.data_classes[data_class_entry] = NetatmoDataClass( + name=data_class_entry, + interval=DEFAULT_INTERVALS[data_class_name], + next_scan=time() + DEFAULT_INTERVALS[data_class_name], + subscriptions=[update_callback], + ) self.data[data_class_entry] = DATA_CLASSES[data_class_name]( self._auth, **kwargs @@ -185,9 +191,9 @@ class NetatmoDataHandler: async def unregister_data_class(self, data_class_entry, update_callback): """Unregister data class.""" - self.data_classes[data_class_entry]["subscriptions"].remove(update_callback) + self.data_classes[data_class_entry].subscriptions.remove(update_callback) - if not self.data_classes[data_class_entry].get("subscriptions"): + if not self.data_classes[data_class_entry].subscriptions: self._queue.remove(self.data_classes[data_class_entry]) self.data_classes.pop(data_class_entry) self.data.pop(data_class_entry) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index b74975f3a62..eb65bc4da0f 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -61,7 +61,7 @@ class NetatmoBase(Entity): data_class["name"], signal_name, self.async_update_callback ) - for sub in self.data_handler.data_classes[signal_name].get("subscriptions"): + for sub in self.data_handler.data_classes[signal_name].subscriptions: if sub is None: await self.data_handler.unregister_data_class(signal_name, None) From 5e9127ef7a076fc8b3441d62571d423ffac16adb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Jul 2021 13:28:01 +0200 Subject: [PATCH 095/818] Remove problematic/redudant db migration happning schema 15 (#52541) --- homeassistant/components/recorder/migration.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e931abbd2d3..4cb42c61097 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -347,7 +347,7 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): ) -def _apply_update(engine, session, new_version, old_version): # noqa: C901 +def _apply_update(engine, session, new_version, old_version): """Perform operations to bring schema up to date.""" connection = session.connection() if new_version == 1: @@ -451,10 +451,8 @@ def _apply_update(engine, session, new_version, old_version): # noqa: C901 elif new_version == 14: _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) elif new_version == 15: - if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - # Recreate the statistics table - Statistics.__table__.drop(engine) - Statistics.__table__.create(engine) + # This dropped the statistics table, done again in version 18. + pass elif new_version == 16: _drop_foreign_key_constraints( connection, engine, TABLE_STATES, ["old_state_id"] From 74029a094821b6368c8e08afe320b73fe9d0ef65 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Jul 2021 13:34:40 +0200 Subject: [PATCH 096/818] Fix Statistics recorder migration path by dropping in pairs (#52453) --- .../components/recorder/migration.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 4cb42c61097..2ed676bfdb9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -461,14 +461,19 @@ def _apply_update(engine, session, new_version, old_version): # This dropped the statistics table, done again in version 18. pass elif new_version == 18: - # Recreate the statisticsmeta tables - if sqlalchemy.inspect(engine).has_table(StatisticsMeta.__tablename__): - StatisticsMeta.__table__.drop(engine) - StatisticsMeta.__table__.create(engine) + # Recreate the statistics and statistics meta tables. + # + # Order matters! Statistics has a relation with StatisticsMeta, + # so statistics need to be deleted before meta (or in pair depending + # on the SQL backend); and meta needs to be created before statistics. + if sqlalchemy.inspect(engine).has_table( + StatisticsMeta.__tablename__ + ) or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + Base.metadata.drop_all( + bind=engine, tables=[Statistics.__table__, StatisticsMeta.__table__] + ) - # Recreate the statistics table - if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - Statistics.__table__.drop(engine) + StatisticsMeta.__table__.create(engine) Statistics.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") From 34782557b77e4a9273a7ccb0edbb93ceeeeb00ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jul 2021 09:17:11 -0500 Subject: [PATCH 097/818] Bump zeroconf to 0.32.1 (#52547) - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.32.0...0.32.1 - Fixes #52384 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 0f7d18446ee..199275623dc 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.32.0"], + "requirements": ["zeroconf==0.32.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e166af6172..98ce2b67183 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.32.0 +zeroconf==0.32.1 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index f242c50d034..e9375f7da07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2427,7 +2427,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.32.0 +zeroconf==0.32.1 # homeassistant.components.zha zha-quirks==0.0.58 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1c13a2e45a..f855044e6d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ yeelight==0.6.3 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.32.0 +zeroconf==0.32.1 # homeassistant.components.zha zha-quirks==0.0.58 From a4b97f7dcb93b3dd38520d8e8241858fe2c53412 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 5 Jul 2021 15:22:41 +0100 Subject: [PATCH 098/818] Update list of supported Coinbase wallet currencies (#52545) --- homeassistant/components/coinbase/const.py | 201 ++++++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 1f86c8026ec..035706c46ce 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -19,50 +19,247 @@ API_ACCOUNTS_DATA = "data" API_RATES = "rates" WALLETS = { + "1INCH": "1INCH", "AAVE": "AAVE", + "ADA": "ADA", + "AED": "AED", + "AFN": "AFN", "ALGO": "ALGO", + "ALL": "ALL", + "AMD": "AMD", + "AMP": "AMP", + "ANG": "ANG", + "ANKR": "ANKR", + "AOA": "AOA", + "ARS": "ARS", "ATOM": "ATOM", + "AUD": "AUD", + "AWG": "AWG", + "AZN": "AZN", "BAL": "BAL", + "BAM": "BAM", "BAND": "BAND", "BAT": "BAT", + "BBD": "BBD", "BCH": "BCH", + "BDT": "BDT", + "BGN": "BGN", + "BHD": "BHD", + "BIF": "BIF", + "BMD": "BMD", + "BND": "BND", "BNT": "BNT", + "BOB": "BOB", + "BOND": "BOND", + "BRL": "BRL", + "BSD": "BSD", "BSV": "BSV", "BTC": "BTC", - "CGLD": "CLGD", - "CVC": "CVC", + "BTN": "BTN", + "BWP": "BWP", + "BYN": "BYN", + "BYR": "BYR", + "BZD": "BZD", + "CAD": "CAD", + "CDF": "CDF", + "CGLD": "CGLD", + "CHF": "CHF", + "CHZ": "CHZ", + "CLF": "CLF", + "CLP": "CLP", + "CNH": "CNH", + "CNY": "CNY", "COMP": "COMP", + "COP": "COP", + "CRC": "CRC", + "CRV": "CRV", + "CTSI": "CTSI", + "CUC": "CUC", + "CVC": "CVC", + "CVE": "CVE", + "CZK": "CZK", "DAI": "DAI", + "DASH": "DASH", + "DJF": "DJF", + "DKK": "DKK", "DNT": "DNT", + "DOGE": "DOGE", + "DOP": "DOP", + "DOT": "DOT", + "DZD": "DZD", + "EGP": "EGP", + "ENJ": "ENJ", "EOS": "EOS", + "ERN": "ERN", + "ETB": "ETB", "ETC": "ETC", "ETH": "ETH", + "ETH2": "ETH2", "EUR": "EUR", "FIL": "FIL", + "FJD": "FJD", + "FKP": "FKP", + "FORTH": "FORTH", "GBP": "GBP", + "GBX": "GBX", + "GEL": "GEL", + "GGP": "GGP", + "GHS": "GHS", + "GIP": "GIP", + "GMD": "GMD", + "GNF": "GNF", "GRT": "GRT", + "GTC": "GTC", + "GTQ": "GTQ", + "GYD": "GYD", + "HKD": "HKD", + "HNL": "HNL", + "HRK": "HRK", + "HTG": "HTG", + "HUF": "HUF", + "ICP": "ICP", + "IDR": "IDR", + "ILS": "ILS", + "IMP": "IMP", + "INR": "INR", + "IQD": "IQD", + "ISK": "ISK", + "JEP": "JEP", + "JMD": "JMD", + "JOD": "JOD", + "JPY": "JPY", + "KEEP": "KEEP", + "KES": "KES", + "KGS": "KGS", + "KHR": "KHR", + "KMF": "KMF", "KNC": "KNC", + "KRW": "KRW", + "KWD": "KWD", + "KYD": "KYD", + "KZT": "KZT", + "LAK": "LAK", + "LBP": "LBP", "LINK": "LINK", + "LKR": "LKR", + "LPT": "LPT", "LRC": "LRC", + "LRD": "LRD", + "LSL": "LSL", "LTC": "LTC", + "LYD": "LYD", + "MAD": "MAD", "MANA": "MANA", + "MATIC": "MATIC", + "MDL": "MDL", + "MGA": "MGA", + "MIR": "MIR", + "MKD": "MKD", "MKR": "MKR", + "MLN": "MLN", + "MMK": "MMK", + "MNT": "MNT", + "MOP": "MOP", + "MRO": "MRO", + "MTL": "MTL", + "MUR": "MUR", + "MVR": "MVR", + "MWK": "MWK", + "MXN": "MXN", + "MYR": "MYR", + "MZN": "MZN", + "NAD": "NAD", + "NGN": "NGN", + "NIO": "NIO", + "NKN": "NKN", "NMR": "NMR", + "NOK": "NOK", + "NPR": "NPR", "NU": "NU", + "NZD": "NZD", + "OGN": "OGN", "OMG": "OMG", + "OMR": "OMR", "OXT": "OXT", + "PAB": "PAB", + "PEN": "PEN", + "PGK": "PGK", + "PHP": "PHP", + "PKR": "PKR", + "PLN": "PLN", + "PYG": "PYG", + "QAR": "QAR", + "QNT": "QNT", + "REN": "REN", "REP": "REP", "REPV2": "REPV2", + "RLC": "RLC", + "RON": "RON", + "RSD": "RSD", + "RUB": "RUB", + "RWF": "RWF", + "SAR": "SAR", + "SBD": "SBD", + "SCR": "SCR", + "SEK": "SEK", + "SGD": "SGD", + "SHP": "SHP", + "SKL": "SKL", + "SLL": "SLL", "SNX": "SNX", + "SOL": "SOL", + "SOS": "SOS", + "SRD": "SRD", + "SSP": "SSP", + "STD": "STD", + "STORJ": "STORJ", + "SUSHI": "SUSHI", + "SVC": "SVC", + "SZL": "SZL", + "THB": "THB", + "TJS": "TJS", + "TMM": "TMM", + "TMT": "TMT", + "TND": "TND", + "TOP": "TOP", + "TRB": "TRB", + "TRY": "TRY", + "TTD": "TTD", + "TWD": "TWD", + "TZS": "TZS", + "UAH": "UAH", + "UGX": "UGX", "UMA": "UMA", "UNI": "UNI", + "USD": "USD", "USDC": "USDC", + "USDT": "USDT", + "UYU": "UYU", + "UZS": "UZS", + "VES": "VES", + "VND": "VND", + "VUV": "VUV", "WBTC": "WBTC", + "WST": "WST", + "XAF": "XAF", + "XAG": "XAG", + "XAU": "XAU", + "XCD": "XCD", + "XDR": "XDR", "XLM": "XLM", + "XOF": "XOF", + "XPD": "XPD", + "XPF": "XPF", + "XPT": "XPT", "XRP": "XRP", "XTZ": "XTZ", + "YER": "YER", "YFI": "YFI", + "ZAR": "ZAR", + "ZEC": "ZEC", + "ZMW": "ZMW", "ZRX": "ZRX", + "ZWL": "ZWL", } RATES = { From f58b231bbd6a8f2e6f6237dfa313553b5272b5ee Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 5 Jul 2021 14:00:32 -0400 Subject: [PATCH 099/818] Bump up zha dependencies (#52555) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index b366b73d6c8..68feabb18b4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,7 +10,7 @@ "zha-quirks==0.0.58", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.35.0", + "zigpy==0.35.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.1" diff --git a/requirements_all.txt b/requirements_all.txt index e9375f7da07..a0048c2fa22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.35.0 +zigpy==0.35.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f855044e6d4..efda5211084 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.35.0 +zigpy==0.35.1 # homeassistant.components.zwave_js zwave-js-server-python==0.27.0 From f74049e018491fba4602016babe5eb3148d84562 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jul 2021 15:00:57 -0500 Subject: [PATCH 100/818] Bump aiohomekit to 0.4.2 (#52560) - Changelog: https://github.com/Jc2k/aiohomekit/compare/0.4.1...0.4.2 - Fixes: #52548 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 39144d6c521..496d629d112 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.4.1"], + "requirements": ["aiohomekit==0.4.2"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index a0048c2fa22..589c599ff34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.1 +aiohomekit==0.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efda5211084..b17986193a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.1 +aiohomekit==0.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 7d87efc99617795969386bfeebf97ce5a488a1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 5 Jul 2021 22:04:55 +0200 Subject: [PATCH 101/818] Bump pysma version to 0.6.2 (#52553) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 85f6de7cb7c..a48b9ba74ce 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.1"], + "requirements": ["pysma==0.6.2"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 589c599ff34..6397fee3b1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1755,7 +1755,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.1 +pysma==0.6.2 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b17986193a3..61eeb4fe488 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -997,7 +997,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.1 +pysma==0.6.2 # homeassistant.components.smappee pysmappee==0.2.25 From 3191fef8d68e555cbfa8c9387a27201b7251bf28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jul 2021 15:16:49 -0500 Subject: [PATCH 102/818] Update the ip/port in the homekit_controller config entry when it changes (#52554) --- .../homekit_controller/config_flow.py | 14 ++++- .../homekit_controller/test_config_flow.py | 57 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index ebb14e43378..e8357a4001d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -236,9 +236,20 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) config_num = None + # Set unique-id and error out if it's already configured + existing_entry = await self.async_set_unique_id(normalize_hkid(hkid)) + updated_ip_port = { + "AccessoryIP": discovery_info["host"], + "AccessoryPort": discovery_info["port"], + } + # If the device is already paired and known to us we should monitor c# # (config_num) for changes. If it changes, we check for new entities if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data={**existing_entry.data, **updated_ip_port} + ) conn = self.hass.data[KNOWN_DEVICES][hkid] # When we rediscover the device, let aiohomekit know # that the device is available and we should not wait @@ -262,8 +273,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_remove(existing.entry_id) # Set unique-id and error out if it's already configured - await self.async_set_unique_id(normalize_hkid(hkid)) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=updated_ip_port) self.context["hkid"] = hkid diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 99c6966e827..52685334500 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for homekit_controller config flow.""" from unittest import mock import unittest.mock -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import aiohomekit from aiohomekit.model import Accessories, Accessory @@ -11,6 +11,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.homekit_controller import config_flow +from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.helpers import device_registry from tests.common import MockConfigEntry, mock_device_registry @@ -383,11 +384,16 @@ async def test_discovery_invalid_config_entry(hass, controller): async def test_discovery_already_configured(hass, controller): """Already configured.""" - MockConfigEntry( + entry = MockConfigEntry( domain="homekit_controller", - data={"AccessoryPairingID": "00:00:00:00:00:00"}, + data={ + "AccessoryIP": "4.4.4.4", + "AccessoryPort": 66, + "AccessoryPairingID": "00:00:00:00:00:00", + }, unique_id="00:00:00:00:00:00", - ).add_to_hass(hass) + ) + entry.add_to_hass(hass) device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) @@ -403,6 +409,49 @@ async def test_discovery_already_configured(hass, controller): ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + assert entry.data["AccessoryIP"] == discovery_info["host"] + assert entry.data["AccessoryPort"] == discovery_info["port"] + + +async def test_discovery_already_configured_update_csharp(hass, controller): + """Already configured and csharp changes.""" + entry = MockConfigEntry( + domain="homekit_controller", + data={ + "AccessoryIP": "4.4.4.4", + "AccessoryPort": 66, + "AccessoryPairingID": "AA:BB:CC:DD:EE:FF", + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + connection_mock = AsyncMock() + connection_mock.pairing.connect.reconnect_soon = AsyncMock() + connection_mock.async_refresh_entity_map = AsyncMock() + hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} + + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Set device as already paired + discovery_info["properties"]["sf"] = 0x00 + discovery_info["properties"]["c#"] = 99999 + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert entry.data["AccessoryIP"] == discovery_info["host"] + assert entry.data["AccessoryPort"] == discovery_info["port"] + assert connection_mock.async_refresh_entity_map.await_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) From 2aacb6b99ffca906af9dcfa0cd7193b4b017b8b5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 6 Jul 2021 00:24:09 +0300 Subject: [PATCH 103/818] Disable flaky shell_command test (#52564) --- tests/components/shell_command/test_init.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index f3a5d46f64b..b86fe12516d 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -5,6 +5,8 @@ import os import tempfile from unittest.mock import MagicMock, patch +import pytest + from homeassistant.components import shell_command from homeassistant.setup import async_setup_component @@ -166,6 +168,7 @@ async def test_stderr_captured(mock_output, hass): assert test_phrase.encode() + b"\n" == mock_output.call_args_list[0][0][-1] +@pytest.mark.skip(reason="disabled to check if it fixes flaky CI") async def test_do_no_run_forever(hass, caplog): """Test subprocesses terminate after the timeout.""" From d249530ed1021ab6712a58a21d076caa1298a113 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 6 Jul 2021 00:09:29 +0000 Subject: [PATCH 104/818] [ci skip] Translation update --- .../components/coinbase/translations/ar.json | 12 ++++++++++++ .../components/simplisafe/translations/et.json | 2 +- .../components/simplisafe/translations/ru.json | 2 +- .../components/simplisafe/translations/zh-Hant.json | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coinbase/translations/ar.json b/homeassistant/components/coinbase/translations/ar.json index 026b7882503..402aaadbd63 100644 --- a/homeassistant/components/coinbase/translations/ar.json +++ b/homeassistant/components/coinbase/translations/ar.json @@ -1,6 +1,18 @@ { + "config": { + "step": { + "user": { + "data": { + "exchange_rates": "\u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641" + }, + "description": "\u064a\u0631\u062c\u0649 \u0625\u062f\u062e\u0627\u0644 \u062a\u0641\u0627\u0635\u064a\u0644 \u0645\u0641\u062a\u0627\u062d API \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0639\u0644\u0649 \u0627\u0644\u0646\u062d\u0648 \u0627\u0644\u0645\u0646\u0635\u0648\u0635 \u0639\u0644\u064a\u0647 \u0645\u0646 \u0642\u0628\u0644 Coinbase.", + "title": "\u062a\u0641\u0627\u0635\u064a\u0644 \u0645\u0641\u062a\u0627\u062d Coinbase API" + } + } + }, "options": { "error": { + "currency_unavaliable": "\u0644\u0627 \u064a\u062a\u0645 \u062a\u0648\u0641\u064a\u0631 \u0648\u0627\u062d\u062f \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0623\u0631\u0635\u062f\u0629 \u0627\u0644\u0639\u0645\u0644\u0627\u062a \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0628\u0648\u0627\u0633\u0637\u0629 Coinbase API \u0627\u0644\u062e\u0627\u0635 \u0628\u0643.", "exchange_rate_unavaliable": "\u0644\u0627 \u064a\u062a\u0645 \u062a\u0648\u0641\u064a\u0631 \u0648\u0627\u062d\u062f \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641 \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0645\u0646 Coinbase.", "unknown": "\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index e815785f0b5..b517a83498a 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -19,7 +19,7 @@ "data": { "password": "Salas\u00f5na" }, - "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto uuesti linkimiseks sisesta oma salas\u00f5na.", + "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto uuesti sidumiseks sisesta salas\u00f5na.", "title": "Taastuvasta SimpliSafe'i konto" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 5fc3fce065e..bcfffc57533 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -19,7 +19,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0421\u0440\u043e\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", + "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 517d48321a8..27064ed1055 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -19,7 +19,7 @@ "data": { "password": "\u5bc6\u78bc" }, - "description": "\u5b58\u53d6\u6b0a\u9650\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", + "description": "\u5b58\u53d6\u6b0a\u6756\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { From a70e8a65fa4ac866062863a9d2f813b088d2bb24 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Jul 2021 02:41:23 -0400 Subject: [PATCH 105/818] Bump pyeight version to 0.1.9 (#52568) --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index fb3762cf738..1c3944a985e 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.1.8"], + "requirements": ["pyeight==0.1.9"], "codeowners": ["@mezz64"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 6397fee3b1d..1bac91ce05c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1408,7 +1408,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.eight_sleep -pyeight==0.1.8 +pyeight==0.1.9 # homeassistant.components.emby pyemby==1.7 From 2a48fe519930a4bd031fd77ceb3ed4724ce3f6d8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 6 Jul 2021 02:47:49 -0400 Subject: [PATCH 106/818] Use entity class attributes for aladdin_connect (#52516) * Use entity class attributes for aladdin_connect * fix * fix * fix pylint --- .../components/aladdin_connect/cover.py | 53 +++++-------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index d4ae9cbb2fd..85f89f3043b 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -8,6 +8,7 @@ from aladdin_connect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( + DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, CoverEntity, ) @@ -61,50 +62,16 @@ def setup_platform( class AladdinDevice(CoverEntity): """Representation of Aladdin Connect cover.""" + _attr_device_class = DEVICE_CLASS_GARAGE + _attr_supported_features = SUPPORTED_FEATURES + def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: """Initialize the cover.""" self._acc = acc self._device_id = device["device_id"] self._number = device["door_number"] - self._name = device["name"] - self._status = STATES_MAP.get(device["status"]) - - @property - def device_class(self) -> str: - """Define this cover as a garage door.""" - return "garage" - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORTED_FEATURES - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._device_id}-{self._number}" - - @property - def name(self) -> str: - """Return the name of the garage door.""" - return self._name - - @property - def is_opening(self) -> bool: - """Return if the cover is opening or not.""" - return self._status == STATE_OPENING - - @property - def is_closing(self) -> bool: - """Return if the cover is closing or not.""" - return self._status == STATE_CLOSING - - @property - def is_closed(self) -> bool | None: - """Return None if status is unknown, True if closed, else False.""" - if self._status is None: - return None - return self._status == STATE_CLOSED + self._attr_name = device["name"] + self._attr_unique_id = f"{self._device_id}-{self._number}" def close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" @@ -116,5 +83,9 @@ class AladdinDevice(CoverEntity): def update(self) -> None: """Update status of cover.""" - acc_status = self._acc.get_door_status(self._device_id, self._number) - self._status = STATES_MAP.get(acc_status) + status = STATES_MAP.get( + self._acc.get_door_status(self._device_id, self._number) + ) + self._attr_is_opening = status == STATE_OPENING + self._attr_is_closing = status == STATE_CLOSING + self._attr_is_closed = None if status is None else status == STATE_CLOSED From b496469a2f936520c7779f7346775e63a87c8dd3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Jul 2021 09:33:00 +0200 Subject: [PATCH 107/818] Fix unavailable entity capable of triggering non-numerical warning in Threshold sensor (#52563) --- .../components/threshold/binary_sensor.py | 5 ++++- .../threshold/test_binary_sensor.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 5bd6f77253b..1a53a599394 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -100,7 +101,9 @@ class ThresholdSensor(BinarySensorEntity): try: self.sensor_value = ( - None if new_state.state == STATE_UNKNOWN else float(new_state.state) + None + if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + else float(new_state.state) ) except (ValueError, TypeError): self.sensor_value = None diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index af8c32a1549..b7c4a871068 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -1,6 +1,11 @@ """The test for the threshold sensor platform.""" -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.setup import async_setup_component @@ -283,7 +288,7 @@ async def test_sensor_in_range_with_hysteresis(hass): assert state.state == "on" -async def test_sensor_in_range_unknown_state(hass): +async def test_sensor_in_range_unknown_state(hass, caplog): """Test if source is within the range.""" config = { "binary_sensor": { @@ -322,6 +327,16 @@ async def test_sensor_in_range_unknown_state(hass): assert state.attributes.get("position") == "unknown" assert state.state == "off" + hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.threshold") + + assert state.attributes.get("position") == "unknown" + assert state.state == "off" + + assert "State is not numerical" not in caplog.text + async def test_sensor_lower_zero_threshold(hass): """Test if a lower threshold of zero is set.""" From e16ef10af5f9f11857e72293fcf065d20df66c70 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 6 Jul 2021 09:54:35 +0200 Subject: [PATCH 108/818] Add type hints to LCN (#52509) * Add type hints to LCN * Fix requested review changes --- .strict-typing | 1 + homeassistant/components/lcn/__init__.py | 41 +++++++---- homeassistant/components/lcn/binary_sensor.py | 55 +++++++++------ homeassistant/components/lcn/climate.py | 57 ++++++++++------ homeassistant/components/lcn/config_flow.py | 12 +++- homeassistant/components/lcn/cover.py | 68 ++++++++++++------- homeassistant/components/lcn/helpers.py | 34 +++++++--- homeassistant/components/lcn/light.py | 58 ++++++++++------ homeassistant/components/lcn/scene.py | 26 +++++-- homeassistant/components/lcn/sensor.py | 46 ++++++++----- homeassistant/components/lcn/services.py | 40 ++++++----- homeassistant/components/lcn/switch.py | 56 +++++++++------ mypy.ini | 11 +++ 13 files changed, 333 insertions(+), 172 deletions(-) diff --git a/.strict-typing b/.strict-typing index d60b99822b9..959aa702672 100644 --- a/.strict-typing +++ b/.strict-typing @@ -48,6 +48,7 @@ homeassistant.components.image_processing.* homeassistant.components.integration.* homeassistant.components.knx.* homeassistant.components.kraken.* +homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.local_ip.* homeassistant.components.lock.* diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 75fd91c28f5..7a6a7c5fab6 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,5 +1,8 @@ """Support for LCN devices.""" +from __future__ import annotations + import logging +from typing import Callable import pypck @@ -14,16 +17,22 @@ from homeassistant.const import ( ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN, PLATFORMS -from .helpers import generate_unique_id, import_lcn_config +from .helpers import ( + DeviceConnectionType, + InputType, + generate_unique_id, + import_lcn_config, +) from .schemas import CONFIG_SCHEMA # noqa: F401 from .services import SERVICES _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the LCN component.""" if DOMAIN not in config: return True @@ -43,7 +52,9 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry( + hass: HomeAssistantType, config_entry: config_entries.ConfigEntry +) -> bool: """Set up a connection to PCHK host from a config entry.""" hass.data.setdefault(DOMAIN, {}) if config_entry.entry_id in hass.data[DOMAIN]: @@ -104,7 +115,9 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry( + hass: HomeAssistantType, config_entry: config_entries.ConfigEntry +) -> bool: """Close connection to PCHK host represented by config_entry.""" # forward unloading to platforms unload_ok = await hass.config_entries.async_unload_platforms( @@ -126,16 +139,18 @@ async def async_unload_entry(hass, config_entry): class LcnEntity(Entity): """Parent class for all entities associated with the LCN component.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN device.""" self.config = config self.entry_id = entry_id self.device_connection = device_connection - self._unregister_for_inputs = None - self._name = config[CONF_NAME] + self._unregister_for_inputs: Callable | None = None + self._name: str = config[CONF_NAME] @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" unique_device_id = generate_unique_id( ( @@ -147,26 +162,26 @@ class LcnEntity(Entity): return f"{self.entry_id}-{unique_device_id}-{self.config[CONF_RESOURCE]}" @property - def should_poll(self): + def should_poll(self) -> bool: """Lcn device entity pushes its state to HA.""" return False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" if not self.device_connection.is_group: self._unregister_for_inputs = self.device_connection.register_for_inputs( self.input_received ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" if self._unregister_for_inputs is not None: self._unregister_for_inputs() @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set state/value when LCN input object (command) is received.""" diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 3bea502cc76..45bd45c8fd7 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -1,21 +1,28 @@ """Support for LCN binary sensors.""" +from __future__ import annotations + import pypck from homeassistant.components.binary_sensor import ( DOMAIN as DOMAIN_BINARY_SENSOR, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, SETPOINTS -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, InputType, get_device_connection -def create_lcn_binary_sensor_entity(hass, entity_config, config_entry): +def create_lcn_binary_sensor_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) if entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] in SETPOINTS: @@ -28,7 +35,11 @@ def create_lcn_binary_sensor_entity(hass, entity_config, config_entry): return LcnLockKeysSensor(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN switch entities from a config entry.""" entities = [] @@ -44,7 +55,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for regulator locks.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN binary sensor.""" super().__init__(config, entry_id, device_connection) @@ -54,7 +67,7 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): self._value = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: @@ -62,7 +75,7 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): self.setpoint_variable ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: @@ -71,11 +84,11 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._value - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" if ( not isinstance(input_obj, pypck.inputs.ModStatusVar) @@ -90,7 +103,9 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): class LcnBinarySensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for binary sensor ports.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN binary sensor.""" super().__init__(config, entry_id, device_connection) @@ -100,7 +115,7 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): self._value = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: @@ -108,7 +123,7 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): self.bin_sensor_port ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: @@ -117,11 +132,11 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._value - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusBinSensors): return @@ -133,31 +148,33 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN sensor for key locks.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN sensor.""" super().__init__(config, entry_id, device_connection) self.source = pypck.lcn_defs.Key[config[CONF_DOMAIN_DATA][CONF_SOURCE]] self._value = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.source) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.source) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._value - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" if ( not isinstance(input_obj, pypck.inputs.ModStatusKeyLocks) diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 056abcda2b0..0a595076a8d 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -1,4 +1,8 @@ """Support for LCN climate control.""" +from __future__ import annotations + +from typing import Any, cast + import pypck from homeassistant.components.climate import ( @@ -6,6 +10,7 @@ from homeassistant.components.climate import ( ClimateEntity, const, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, @@ -14,6 +19,8 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import ( @@ -23,21 +30,27 @@ from .const import ( CONF_MIN_TEMP, CONF_SETPOINT, ) -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, InputType, get_device_connection PARALLEL_UPDATES = 0 -def create_lcn_climate_entity(hass, entity_config, config_entry): +def create_lcn_climate_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) return LcnClimate(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN switch entities from a config entry.""" entities = [] @@ -53,7 +66,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnClimate(LcnEntity, ClimateEntity): """Representation of a LCN climate device.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize of a LCN climate device.""" super().__init__(config, entry_id, device_connection) @@ -72,14 +87,14 @@ class LcnClimate(LcnEntity, ClimateEntity): self._target_temperature = None self._is_on = True - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.variable) await self.device_connection.activate_status_request_handler(self.setpoint) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: @@ -87,27 +102,27 @@ class LcnClimate(LcnEntity, ClimateEntity): await self.device_connection.cancel_status_request_handler(self.setpoint) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return const.SUPPORT_TARGET_TEMPERATURE @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - return self.unit.value + return cast(str, self.unit.value) @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temperature @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -117,7 +132,7 @@ class LcnClimate(LcnEntity, ClimateEntity): return const.HVAC_MODE_OFF @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. @@ -128,16 +143,16 @@ class LcnClimate(LcnEntity, ClimateEntity): return modes @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" - return self._max_temp + return cast(float, self._max_temp) @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" - return self._min_temp + return cast(float, self._min_temp) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == const.HVAC_MODE_HEAT: if not await self.device_connection.lock_regulator( @@ -153,7 +168,7 @@ class LcnClimate(LcnEntity, ClimateEntity): self._target_temperature = None self.async_write_ha_state() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -166,7 +181,7 @@ class LcnClimate(LcnEntity, ClimateEntity): self._target_temperature = temperature self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set temperature value when LCN input object is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusVar): return diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 698a4dcedfe..9ac8cc7f1fa 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the LCN integration.""" +from __future__ import annotations + import logging import pypck @@ -11,13 +13,17 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN _LOGGER = logging.getLogger(__name__) -def get_config_entry(hass, data): +def get_config_entry( + hass: HomeAssistantType, data: ConfigType +) -> config_entries.ConfigEntry | None: """Check config entries for already configured entries based on the ip address/port.""" return next( ( @@ -30,7 +36,7 @@ def get_config_entry(hass, data): ) -async def validate_connection(host_name, data): +async def validate_connection(host_name: str, data: ConfigType) -> ConfigType: """Validate if a connection to LCN can be established.""" host = data[CONF_IP_ADDRESS] port = data[CONF_PORT] @@ -62,7 +68,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, data): + async def async_step_import(self, data: ConfigType) -> FlowResult: """Import existing configuration from LCN.""" host_name = data[CONF_HOST] # validate the imported connection parameters diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index bf777ad93f2..608881435dc 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,21 +1,29 @@ """Support for LCN covers.""" +from __future__ import annotations + +from typing import Any import pypck from homeassistant.components.cover import DOMAIN as DOMAIN_COVER, CoverEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import CONF_DOMAIN_DATA, CONF_MOTOR, CONF_REVERSE_TIME -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, InputType, get_device_connection PARALLEL_UPDATES = 0 -def create_lcn_cover_entity(hass, entity_config, config_entry): +def create_lcn_cover_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) if entity_config[CONF_DOMAIN_DATA][CONF_MOTOR] in "OUTPUTS": @@ -24,7 +32,11 @@ def create_lcn_cover_entity(hass, entity_config, config_entry): return LcnRelayCover(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN cover entities from a config entry.""" entities = [] @@ -38,7 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnOutputsCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to output ports.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN cover.""" super().__init__(config, entry_id, device_connection) @@ -57,7 +71,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): self._is_closing = False self._is_opening = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: @@ -68,7 +82,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): pypck.lcn_defs.OutputPort["OUTPUTDOWN"] ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: @@ -80,26 +94,26 @@ class LcnOutputsCover(LcnEntity, CoverEntity): ) @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self._is_closed @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._is_opening @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._is_closing @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" return True - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" state = pypck.lcn_defs.MotorStateModifier.DOWN if not await self.device_connection.control_motors_outputs( @@ -110,7 +124,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): self._is_closing = True self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" state = pypck.lcn_defs.MotorStateModifier.UP if not await self.device_connection.control_motors_outputs( @@ -122,7 +136,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): self._is_closing = False self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" state = pypck.lcn_defs.MotorStateModifier.STOP if not await self.device_connection.control_motors_outputs(state): @@ -131,7 +145,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): self._is_opening = False self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set cover states when LCN input object (command) is received.""" if ( not isinstance(input_obj, pypck.inputs.ModStatusOutput) @@ -159,7 +173,9 @@ class LcnOutputsCover(LcnEntity, CoverEntity): class LcnRelayCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to relays.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN cover.""" super().__init__(config, entry_id, device_connection) @@ -171,39 +187,39 @@ class LcnRelayCover(LcnEntity, CoverEntity): self._is_closing = False self._is_opening = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.motor) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.motor) @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self._is_closed @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._is_opening @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._is_closing @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" return True - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN @@ -213,7 +229,7 @@ class LcnRelayCover(LcnEntity, CoverEntity): self._is_closing = True self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP @@ -224,7 +240,7 @@ class LcnRelayCover(LcnEntity, CoverEntity): self._is_closing = False self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP @@ -234,7 +250,7 @@ class LcnRelayCover(LcnEntity, CoverEntity): self._is_opening = False self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set cover states when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusRelays): return diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 0687491b052..07e3866cd48 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -1,9 +1,13 @@ """Helpers for LCN component.""" +from __future__ import annotations + import re +from typing import Tuple, Type, Union, cast import pypck import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ADDRESS, CONF_BINARY_SENSORS, @@ -21,6 +25,7 @@ from homeassistant.const import ( CONF_SWITCHES, CONF_USERNAME, ) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( CONF_CLIMATES, @@ -38,6 +43,13 @@ from .const import ( DOMAIN, ) +# typing +AddressType = Tuple[int, int, bool] +DeviceConnectionType = Union[ + pypck.module.ModuleConnection, pypck.module.GroupConnection +] +InputType = Type[pypck.inputs.Input] + # Regex for address validation PATTERN_ADDRESS = re.compile( "^((?P\\w+)\\.)?s?(?P\\d+)\\.(?Pm|g)?(?P\\d+)$" @@ -55,21 +67,23 @@ DOMAIN_LOOKUP = { } -def get_device_connection(hass, address, config_entry): +def get_device_connection( + hass: HomeAssistantType, address: AddressType, config_entry: ConfigEntry +) -> DeviceConnectionType | None: """Return a lcn device_connection.""" host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] addr = pypck.lcn_addr.LcnAddr(*address) return host_connection.get_address_conn(addr) -def get_resource(domain_name, domain_data): +def get_resource(domain_name: str, domain_data: ConfigType) -> str: """Return the resource for the specified domain_data.""" if domain_name in ["switch", "light"]: - return domain_data["output"] + return cast(str, domain_data["output"]) if domain_name in ["binary_sensor", "sensor"]: - return domain_data["source"] + return cast(str, domain_data["source"]) if domain_name == "cover": - return domain_data["motor"] + return cast(str, domain_data["motor"]) if domain_name == "climate": return f'{domain_data["source"]}.{domain_data["setpoint"]}' if domain_name == "scene": @@ -77,13 +91,13 @@ def get_resource(domain_name, domain_data): raise ValueError("Unknown domain") -def generate_unique_id(address): +def generate_unique_id(address: AddressType) -> str: """Generate a unique_id from the given parameters.""" is_group = "g" if address[2] else "m" return f"{is_group}{address[0]:03d}{address[1]:03d}" -def import_lcn_config(lcn_config): +def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]: """Convert lcn settings from configuration.yaml to config_entries data. Create a list of config_entry data structures like: @@ -185,7 +199,7 @@ def import_lcn_config(lcn_config): return list(data.values()) -def has_unique_host_names(hosts): +def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]: """Validate that all connection names are unique. Use 'pchk' as default connection_name (or add a numeric suffix if @@ -206,7 +220,7 @@ def has_unique_host_names(hosts): return hosts -def is_address(value): +def is_address(value: str) -> tuple[AddressType, str]: """Validate the given address string. Examples for S000M005 at myhome: @@ -227,7 +241,7 @@ def is_address(value): raise ValueError(f"{value} is not a valid address string") -def is_states_string(states_string): +def is_states_string(states_string: str) -> list[str]: """Validate the given states string and return states list.""" if len(states_string) != 8: raise ValueError("Invalid length of states string") diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 8697d8e0319..2a2d050143c 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,4 +1,7 @@ """Support for LCN lights.""" +from __future__ import annotations + +from typing import Any import pypck @@ -10,7 +13,10 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import ( @@ -20,15 +26,17 @@ from .const import ( CONF_TRANSITION, OUTPUT_PORTS, ) -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, InputType, get_device_connection PARALLEL_UPDATES = 0 -def create_lcn_light_entity(hass, entity_config, config_entry): +def create_lcn_light_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS: @@ -37,7 +45,11 @@ def create_lcn_light_entity(hass, entity_config, config_entry): return LcnRelayLight(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN light entities from a config entry.""" entities = [] @@ -51,7 +63,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnOutputLight(LcnEntity, LightEntity): """Representation of a LCN light for output ports.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN light.""" super().__init__(config, entry_id, device_connection) @@ -66,36 +80,36 @@ class LcnOutputLight(LcnEntity, LightEntity): self._is_on = False self._is_dimming_to_zero = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.output) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" if self.dimmable: return SUPPORT_TRANSITION | SUPPORT_BRIGHTNESS return SUPPORT_TRANSITION @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return self._brightness @property - def is_on(self): + def is_on(self) -> bool: """Return True if entity is on.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if ATTR_BRIGHTNESS in kwargs: percent = int(kwargs[ATTR_BRIGHTNESS] / 255.0 * 100) @@ -116,7 +130,7 @@ class LcnOutputLight(LcnEntity, LightEntity): self._is_dimming_to_zero = False self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if ATTR_TRANSITION in kwargs: transition = pypck.lcn_defs.time_to_ramp_value( @@ -133,7 +147,7 @@ class LcnOutputLight(LcnEntity, LightEntity): self._is_on = False self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set light state when LCN input object (command) is received.""" if ( not isinstance(input_obj, pypck.inputs.ModStatusOutput) @@ -144,7 +158,7 @@ class LcnOutputLight(LcnEntity, LightEntity): self._brightness = int(input_obj.get_percent() / 100.0 * 255) if self.brightness == 0: self._is_dimming_to_zero = False - if not self._is_dimming_to_zero: + if not self._is_dimming_to_zero and self.brightness is not None: self._is_on = self.brightness > 0 self.async_write_ha_state() @@ -152,7 +166,9 @@ class LcnOutputLight(LcnEntity, LightEntity): class LcnRelayLight(LcnEntity, LightEntity): """Representation of a LCN light for relay ports.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN light.""" super().__init__(config, entry_id, device_connection) @@ -160,24 +176,24 @@ class LcnRelayLight(LcnEntity, LightEntity): self._is_on = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.output) @property - def is_on(self): + def is_on(self) -> bool: """Return True if entity is on.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON @@ -186,7 +202,7 @@ class LcnRelayLight(LcnEntity, LightEntity): self._is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF @@ -195,7 +211,7 @@ class LcnRelayLight(LcnEntity, LightEntity): self._is_on = False self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set light state when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusRelays): return diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index 8f770df7668..d3faa887dc8 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -1,9 +1,15 @@ """Support for LCN scenes.""" +from __future__ import annotations + +from typing import Any import pypck from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import ( @@ -13,21 +19,27 @@ from .const import ( CONF_TRANSITION, OUTPUT_PORTS, ) -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, get_device_connection PARALLEL_UPDATES = 0 -def create_lcn_scene_entity(hass, entity_config, config_entry): +def create_lcn_scene_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) return LcnScene(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN switch entities from a config entry.""" entities = [] @@ -41,7 +53,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnScene(LcnEntity, Scene): """Representation of a LCN scene.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN scene.""" super().__init__(config, entry_id, device_connection) @@ -63,7 +77,7 @@ class LcnScene(LcnEntity, Scene): config[CONF_DOMAIN_DATA][CONF_TRANSITION] ) - async def async_activate(self, **kwargs): + async def async_activate(self, **kwargs: Any) -> None: """Activate scene.""" await self.device_connection.activate_scene( self.register_id, diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 64870e22e4c..740319417c3 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,8 +1,10 @@ """Support for LCN sensors.""" +from __future__ import annotations import pypck from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ADDRESS, CONF_DOMAIN, @@ -10,6 +12,8 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import ( @@ -20,13 +24,15 @@ from .const import ( THRESHOLDS, VARIABLES, ) -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, InputType, get_device_connection -def create_lcn_sensor_entity(hass, entity_config, config_entry): +def create_lcn_sensor_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) if ( @@ -40,7 +46,11 @@ def create_lcn_sensor_entity(hass, entity_config, config_entry): return LcnLedLogicSensor(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN switch entities from a config entry.""" entities = [] @@ -54,7 +64,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnVariableSensor(LcnEntity, SensorEntity): """Representation of a LCN sensor for variables.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN sensor.""" super().__init__(config, entry_id, device_connection) @@ -65,29 +77,29 @@ class LcnVariableSensor(LcnEntity, SensorEntity): self._value = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.variable) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.variable) @property - def state(self): + def state(self) -> str | None: """Return the state of the entity.""" return self._value @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return self.unit.value + return str(self.unit.value) - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" if ( not isinstance(input_obj, pypck.inputs.ModStatusVar) @@ -102,7 +114,9 @@ class LcnVariableSensor(LcnEntity, SensorEntity): class LcnLedLogicSensor(LcnEntity, SensorEntity): """Representation of a LCN sensor for leds and logicops.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN sensor.""" super().__init__(config, entry_id, device_connection) @@ -115,24 +129,24 @@ class LcnLedLogicSensor(LcnEntity, SensorEntity): self._value = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.source) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.source) @property - def state(self): + def state(self) -> str | None: """Return the state of the entity.""" return self._value - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusLedsAndLogicOps): return diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index fa74f556593..7df6f4ce2ed 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -12,6 +12,7 @@ from homeassistant.const import ( TIME_SECONDS, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType from .const import ( CONF_KEYS, @@ -40,7 +41,12 @@ from .const import ( VAR_UNITS, VARIABLES, ) -from .helpers import get_device_connection, is_address, is_states_string +from .helpers import ( + DeviceConnectionType, + get_device_connection, + is_address, + is_states_string, +) class LcnServiceCall: @@ -48,11 +54,11 @@ class LcnServiceCall: schema = vol.Schema({vol.Required(CONF_ADDRESS): is_address}) - def __init__(self, hass): + def __init__(self, hass: HomeAssistantType) -> None: """Initialize service call.""" self.hass = hass - def get_device_connection(self, service): + def get_device_connection(self, service: ServiceCallType) -> DeviceConnectionType: """Get address connection object.""" address, host_name = service.data[CONF_ADDRESS] @@ -66,7 +72,7 @@ class LcnServiceCall: return device_connection raise ValueError("Invalid host name.") - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" raise NotImplementedError @@ -86,7 +92,7 @@ class OutputAbs(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] brightness = service.data[CONF_BRIGHTNESS] @@ -110,7 +116,7 @@ class OutputRel(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] brightness = service.data[CONF_BRIGHTNESS] @@ -131,7 +137,7 @@ class OutputToggle(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] transition = pypck.lcn_defs.time_to_ramp_value( @@ -147,7 +153,7 @@ class Relays(LcnServiceCall): schema = LcnServiceCall.schema.extend({vol.Required(CONF_STATE): is_states_string}) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" states = [ pypck.lcn_defs.RelayStateModifier[state] @@ -168,7 +174,7 @@ class Led(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" led = pypck.lcn_defs.LedPort[service.data[CONF_LED]] led_state = pypck.lcn_defs.LedStatus[service.data[CONF_STATE]] @@ -196,7 +202,7 @@ class VarAbs(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] value = service.data[CONF_VALUE] @@ -213,7 +219,7 @@ class VarReset(LcnServiceCall): {vol.Required(CONF_VARIABLE): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS))} ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] @@ -239,7 +245,7 @@ class VarRel(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] value = service.data[CONF_VALUE] @@ -260,7 +266,7 @@ class LockRegulator(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" setpoint = pypck.lcn_defs.Var[service.data[CONF_SETPOINT]] state = service.data[CONF_STATE] @@ -288,7 +294,7 @@ class SendKeys(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" device_connection = self.get_device_connection(service) @@ -331,7 +337,7 @@ class LockKeys(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" device_connection = self.get_device_connection(service) @@ -368,7 +374,7 @@ class DynText(LcnServiceCall): } ) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" row_id = service.data[CONF_ROW] - 1 text = service.data[CONF_TEXT] @@ -382,7 +388,7 @@ class Pck(LcnServiceCall): schema = LcnServiceCall.schema.extend({vol.Required(CONF_PCK): str}) - async def async_call_service(self, service): + async def async_call_service(self, service: ServiceCallType) -> None: """Execute service call.""" pck = service.data[CONF_PCK] device_connection = self.get_device_connection(service) diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 1429bf67f7e..f5159b3492d 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,21 +1,29 @@ """Support for LCN switches.""" +from __future__ import annotations + +from typing import Any import pypck from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import LcnEntity from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS -from .helpers import get_device_connection +from .helpers import DeviceConnectionType, InputType, get_device_connection PARALLEL_UPDATES = 0 -def create_lcn_switch_entity(hass, entity_config, config_entry): +def create_lcn_switch_entity( + hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry +) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( - hass, tuple(entity_config[CONF_ADDRESS]), config_entry + hass, entity_config[CONF_ADDRESS], config_entry ) if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS: @@ -24,7 +32,11 @@ def create_lcn_switch_entity(hass, entity_config, config_entry): return LcnRelaySwitch(entity_config, config_entry.entry_id, device_connection) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up LCN switch entities from a config entry.""" entities = [] @@ -39,46 +51,48 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LcnOutputSwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for output ports.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN switch.""" super().__init__(config, entry_id, device_connection) self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - self._is_on = None + self._is_on = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.output) @property - def is_on(self): + def is_on(self) -> bool: """Return True if entity is on.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if not await self.device_connection.dim_output(self.output.value, 100, 0): return self._is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if not await self.device_connection.dim_output(self.output.value, 0, 0): return self._is_on = False self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set switch state when LCN input object (command) is received.""" if ( not isinstance(input_obj, pypck.inputs.ModStatusOutput) @@ -93,32 +107,34 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): class LcnRelaySwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for relay ports.""" - def __init__(self, config, entry_id, device_connection): + def __init__( + self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType + ) -> None: """Initialize the LCN switch.""" super().__init__(config, entry_id, device_connection) self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - self._is_on = None + self._is_on = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() if not self.device_connection.is_group: await self.device_connection.cancel_status_request_handler(self.output) @property - def is_on(self): + def is_on(self) -> bool: """Return True if entity is on.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON @@ -127,7 +143,7 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): self._is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF @@ -136,7 +152,7 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): self._is_on = False self.async_write_ha_state() - def input_received(self, input_obj): + def input_received(self, input_obj: InputType) -> None: """Set switch state when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusRelays): return diff --git a/mypy.ini b/mypy.ini index 521e5cc4b7d..a3c856f546c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -539,6 +539,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lcn.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.light.*] check_untyped_defs = true disallow_incomplete_defs = true From 33577e1bfceb4f32f97db7c50bb60fd6c47582b1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Jul 2021 11:52:53 +0200 Subject: [PATCH 109/818] Update frontend to 20210706.0 (#52577) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 42ade2d4ae9..c7283a9503a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210630.0" + "home-assistant-frontend==20210706.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98ce2b67183..c35ff252b52 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210630.0 +home-assistant-frontend==20210706.0 httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1bac91ce05c..b9662549ab8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -779,7 +779,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210630.0 +home-assistant-frontend==20210706.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61eeb4fe488..5e2477db357 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210630.0 +home-assistant-frontend==20210706.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 669aca9585268538d853eaa77aef46f39a937e0f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 6 Jul 2021 12:32:17 +0200 Subject: [PATCH 110/818] Bump gios to version 1.0.2 (#52576) --- homeassistant/components/gios/manifest.json | 2 +- homeassistant/components/gios/sensor.py | 32 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gios/__init__.py | 6 +- tests/components/gios/test_sensor.py | 168 +++++++++++++++++++- 6 files changed, 200 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 3dfb2a168db..f13da0e3f33 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==1.0.1"], + "requirements": ["gios==1.0.2"], "config_flow": true, "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index c0bda830f25..b9c1290dc00 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -13,6 +13,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import GiosDataUpdateCoordinator from .const import ( + ATTR_AQI, ATTR_INDEX, ATTR_STATION, ATTR_UNIT, @@ -33,10 +34,14 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id] - sensors = [] + sensors: list[GiosSensor | GiosAqiSensor] = [] - for sensor in coordinator.data: - if sensor in SENSOR_TYPES: + for sensor, sensor_data in coordinator.data.items(): + if sensor not in SENSOR_TYPES or not sensor_data.get(ATTR_VALUE): + continue + if sensor == ATTR_AQI: + sensors.append(GiosAqiSensor(name, sensor, coordinator)) + else: sensors.append(GiosSensor(name, sensor, coordinator)) async_add_entities(sensors) @@ -84,6 +89,21 @@ class GiosSensor(CoordinatorEntity, SensorEntity): def state(self) -> StateType: """Return the state.""" self._state = self.coordinator.data[self._sensor_type][ATTR_VALUE] - if self._description.get(ATTR_VALUE): - return cast(StateType, self._description[ATTR_VALUE](self._state)) - return cast(StateType, self._state) + return cast(StateType, self._description[ATTR_VALUE](self._state)) + + +class GiosAqiSensor(GiosSensor): + """Define an GIOS AQI sensor.""" + + @property + def state(self) -> StateType: + """Return the state.""" + return cast(StateType, self.coordinator.data[self._sensor_type][ATTR_VALUE]) + + @property + def available(self) -> bool: + """Return if entity is available.""" + available = super().available + return available and bool( + self.coordinator.data[self._sensor_type].get(ATTR_VALUE) + ) diff --git a/requirements_all.txt b/requirements_all.txt index b9662549ab8..d1919653e57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -677,7 +677,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==1.0.1 +gios==1.0.2 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e2477db357..c5658bf9b5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,7 +383,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==1.0.1 +gios==1.0.2 # homeassistant.components.glances glances_api==0.2.0 diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 537d6265125..6c39ee35303 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -12,7 +12,9 @@ STATIONS = [ ] -async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: +async def init_integration( + hass, incomplete_data=False, invalid_indexes=False +) -> MockConfigEntry: """Set up the GIOS integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -28,6 +30,8 @@ async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: indexes["stIndexLevel"]["indexLevelName"] = "foo" sensors["pm10"]["values"][0]["value"] = None sensors["pm10"]["values"][1]["value"] = None + if invalid_indexes: + indexes = {} with patch( "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 4d43d90f9a2..8ce192b7e9c 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from gios import ApiError -from homeassistant.components.gios.const import ATTR_STATION, ATTRIBUTION +from homeassistant.components.gios.const import ATTR_INDEX, ATTR_STATION, ATTRIBUTION from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -37,6 +37,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "bardzo dobry" entry = registry.async_get("sensor.home_c6h6") assert entry @@ -53,6 +54,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_co") assert entry @@ -69,6 +71,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_no2") assert entry @@ -85,6 +88,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_o3") assert entry @@ -101,6 +105,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_pm10") assert entry @@ -117,6 +122,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_pm2_5") assert entry @@ -133,6 +139,7 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) == "bardzo dobry" entry = registry.async_get("sensor.home_so2") assert entry @@ -179,7 +186,7 @@ async def test_availability(hass): return_value=json.loads(load_fixture("gios/sensors.json")), ), patch( "homeassistant.components.gios.Gios._get_indexes", - return_value=json.loads(load_fixture("gios/indexes.json")), + return_value={}, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -188,3 +195,160 @@ async def test_availability(hass): assert state assert state.state != STATE_UNAVAILABLE assert state.state == "4" + + state = hass.states.get("sensor.home_aqi") + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_invalid_indexes(hass): + """Test states of the sensor when API returns invalid indexes.""" + await init_integration(hass, invalid_indexes=True) + registry = er.async_get(hass) + + state = hass.states.get("sensor.home_c6h6") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_c6h6") + assert entry + assert entry.unique_id == "123-c6h6" + + state = hass.states.get("sensor.home_co") + assert state + assert state.state == "252" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_co") + assert entry + assert entry.unique_id == "123-co" + + state = hass.states.get("sensor.home_no2") + assert state + assert state.state == "7" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_no2") + assert entry + assert entry.unique_id == "123-no2" + + state = hass.states.get("sensor.home_o3") + assert state + assert state.state == "96" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_o3") + assert entry + assert entry.unique_id == "123-o3" + + state = hass.states.get("sensor.home_pm10") + assert state + assert state.state == "17" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_pm10") + assert entry + assert entry.unique_id == "123-pm10" + + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == "4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_pm2_5") + assert entry + assert entry.unique_id == "123-pm2.5" + + state = hass.states.get("sensor.home_so2") + assert state + assert state.state == "4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_INDEX) is None + + entry = registry.async_get("sensor.home_so2") + assert entry + assert entry.unique_id == "123-so2" + + state = hass.states.get("sensor.home_aqi") + assert state is None + + +async def test_aqi_sensor_availability(hass): + """Ensure that we mark the AQI sensor unavailable correctly when indexes are invalid.""" + await init_integration(hass) + + state = hass.states.get("sensor.home_aqi") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "dobry" + + future = utcnow() + timedelta(minutes=60) + with patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=json.loads(load_fixture("gios/sensors.json")), + ), patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value={}, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_aqi") + assert state + assert state.state == STATE_UNAVAILABLE From 94aa292c9baaa5a51936f4738eb95d4c8bf1505c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 6 Jul 2021 12:49:46 +0200 Subject: [PATCH 111/818] Wheels v2021.07.0 (#52580) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8e97c61194b..cce57401ea8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,7 +81,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2021.06.0 + uses: home-assistant/wheels@2021.07.0 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} @@ -150,7 +150,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2021.06.0 + uses: home-assistant/wheels@2021.07.0 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} From b645560633f49eea5fa0574ca4e7f18d7c80bf24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Jul 2021 13:29:39 +0200 Subject: [PATCH 112/818] Minor improvements of util.percentage typing (#52581) --- homeassistant/util/percentage.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index 42beeeb5523..260c4f374fe 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -1,8 +1,12 @@ """Percentage util functions.""" from __future__ import annotations +from typing import TypeVar -def ordered_list_item_to_percentage(ordered_list: list[str], item: str) -> int: +T = TypeVar("T") + + +def ordered_list_item_to_percentage(ordered_list: list[T], item: T) -> int: """Determine the percentage of an item in an ordered list. When using this utility for fan speeds, do not include "off" @@ -25,7 +29,7 @@ def ordered_list_item_to_percentage(ordered_list: list[str], item: str) -> int: return (list_position * 100) // list_len -def percentage_to_ordered_list_item(ordered_list: list[str], percentage: int) -> str: +def percentage_to_ordered_list_item(ordered_list: list[T], percentage: int) -> T: """Find the item that most closely matches the percentage in an ordered list. When using this utility for fan speeds, do not include "off" From dc72c6c606915c2247c0b35dbe6fac2b0c6a25bc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 6 Jul 2021 14:04:00 +0200 Subject: [PATCH 113/818] Improve config entry title for GIOS integration (#52583) * Improve GIOS config entry title * Usonly station name as title --- homeassistant/components/gios/config_flow.py | 2 +- tests/components/gios/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index 161dc1b0add..b8a52254c32 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -49,7 +49,7 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await gios.async_update() return self.async_create_entry( - title=user_input[CONF_STATION_ID], + title=gios.station_name, data=user_input, ) except (ApiError, ClientConnectorError, asyncio.TimeoutError): diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 6b1f829c4d8..21fc9d8bfda 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -99,7 +99,7 @@ async def test_create_entry(hass): result = await flow.async_step_user(user_input=CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONFIG[CONF_STATION_ID] + assert result["title"] == "Test Name 1" assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] assert flow.context["unique_id"] == "123" From 4d32e1ed01d6bd6cbf52e193acb33fb8e14d8462 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Jul 2021 14:38:48 +0200 Subject: [PATCH 114/818] Minor improvements of MQTT typing (#52578) * Minor improvements of MQTT typing * Tweak --- homeassistant/components/axis/device.py | 4 +-- homeassistant/components/mqtt/__init__.py | 29 ++++++++++++------- homeassistant/components/mqtt/mixins.py | 6 ++-- homeassistant/components/mqtt/models.py | 21 ++++++++++---- homeassistant/components/mqtt/subscription.py | 6 ++-- homeassistant/components/mysensors/gateway.py | 8 ++--- tests/common.py | 4 +-- tests/components/tasmota/test_config_flow.py | 10 +++++-- 8 files changed, 56 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index f1a57eec33c..e4987c77139 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -12,7 +12,7 @@ from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN -from homeassistant.components.mqtt.models import Message +from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -195,7 +195,7 @@ class AxisNetworkDevice: ) @callback - def mqtt_message(self, message: Message) -> None: + def mqtt_message(self, message: ReceiveMessage) -> None: """Receive Axis MQTT message.""" self.disconnect_from_stream() diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9883e7b6ec8..e524502dd8d 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -75,9 +75,11 @@ from .const import ( from .discovery import LAST_DISCOVERY from .models import ( AsyncMessageCallbackType, - Message, MessageCallbackType, + PublishMessage, PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic @@ -290,9 +292,9 @@ def async_publish_template( AsyncDeprecatedMessageCallbackType = Callable[ - [str, PublishPayloadType, int], Awaitable[None] + [str, ReceivePayloadType, int], Awaitable[None] ] -DeprecatedMessageCallbackType = Callable[[str, PublishPayloadType, int], None] +DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] def wrap_msg_callback( @@ -308,7 +310,7 @@ def wrap_msg_callback( if asyncio.iscoroutinefunction(check_func): @wraps(msg_callback) - async def async_wrapper(msg: Message) -> None: + async def async_wrapper(msg: ReceiveMessage) -> None: """Call with deprecated signature.""" await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( msg.topic, msg.payload, msg.qos @@ -318,7 +320,7 @@ def wrap_msg_callback( else: @wraps(msg_callback) - def wrapper(msg: Message) -> None: + def wrapper(msg: ReceiveMessage) -> None: """Call with deprecated signature.""" msg_callback(msg.topic, msg.payload, msg.qos) @@ -676,7 +678,7 @@ class MQTT: CONF_WILL_MESSAGE in self.conf and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] ): - will_message = Message(**self.conf[CONF_WILL_MESSAGE]) + will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) else: will_message = None @@ -853,7 +855,7 @@ class MQTT: retain=birth_message.retain, ) - birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) + birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) asyncio.run_coroutine_threadsafe( publish_birth_message(birth_message), self.hass.loop ) @@ -900,7 +902,7 @@ class MQTT: self.hass.async_run_hass_job( subscription.job, - Message( + ReceiveMessage( msg.topic, payload, msg.qos, @@ -1043,7 +1045,7 @@ async def websocket_subscribe(hass, connection, msg): if not connection.user.is_admin: raise Unauthorized - async def forward_messages(mqttmsg: Message): + async def forward_messages(mqttmsg: ReceiveMessage): """Forward events to websocket.""" connection.send_message( websocket_api.event_message( @@ -1064,8 +1066,13 @@ async def websocket_subscribe(hass, connection, msg): connection.send_message(websocket_api.result_message(msg["id"])) +ConnectionStatusCallback = Callable[[bool], None] + + @callback -def async_subscribe_connection_status(hass, connection_status_callback): +def async_subscribe_connection_status( + hass: HomeAssistant, connection_status_callback: ConnectionStatusCallback +) -> Callable[[], None]: """Subscribe to MQTT connection changes.""" connection_status_callback_job = HassJob(connection_status_callback) @@ -1092,6 +1099,6 @@ def async_subscribe_connection_status(hass, connection_status_callback): return unsubscribe -def is_connected(hass): +def is_connected(hass: HomeAssistant) -> bool: """Return if MQTT client is connected.""" return hass.data[DATA_MQTT].connected diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9e45d6d4f27..a40f06a3bb6 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -38,7 +38,7 @@ from .discovery import ( clear_discovery_hash, set_discovery_hash, ) -from .models import Message +from .models import ReceiveMessage from .subscription import async_subscribe_topics, async_unsubscribe_topics from .util import valid_subscribe_topic @@ -221,7 +221,7 @@ class MqttAttributes(Entity): @callback @log_messages(self.hass, self.entity_id) - def attributes_message_received(msg: Message) -> None: + def attributes_message_received(msg: ReceiveMessage) -> None: try: payload = msg.payload if attr_tpl is not None: @@ -318,7 +318,7 @@ class MqttAvailability(Entity): @callback @log_messages(self.hass, self.entity_id) - def availability_message_received(msg: Message) -> None: + def availability_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT availability message.""" topic = msg.topic if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 0c8c311d768..5c320ac0827 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -7,19 +7,30 @@ from typing import Awaitable, Callable, Union import attr PublishPayloadType = Union[str, bytes, int, float, None] +ReceivePayloadType = Union[str, bytes] @attr.s(slots=True, frozen=True) -class Message: +class PublishMessage: """MQTT Message.""" topic: str = attr.ib() payload: PublishPayloadType = attr.ib() qos: int = attr.ib() retain: bool = attr.ib() - subscribed_topic: str | None = attr.ib(default=None) - timestamp: dt.datetime | None = attr.ib(default=None) -AsyncMessageCallbackType = Callable[[Message], Awaitable[None]] -MessageCallbackType = Callable[[Message], None] +@attr.s(slots=True, frozen=True) +class ReceiveMessage: + """MQTT Message.""" + + topic: str = attr.ib() + payload: ReceivePayloadType = attr.ib() + qos: int = attr.ib() + retain: bool = attr.ib() + subscribed_topic: str = attr.ib(default=None) + timestamp: dt.datetime = attr.ib(default=None) + + +AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] +MessageCallbackType = Callable[[ReceiveMessage], None] diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 6c711600b2c..03259a37380 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -66,7 +66,7 @@ async def async_subscribe_topics( hass: HomeAssistant, new_state: dict[str, EntitySubscription] | None, topics: dict[str, Any], -): +) -> dict[str, EntitySubscription]: """(Re)Subscribe to a set of MQTT topics. State is kept in sub_state and a dictionary mapping from the subscription @@ -106,6 +106,8 @@ async def async_subscribe_topics( @bind_hass -async def async_unsubscribe_topics(hass: HomeAssistant, sub_state: dict): +async def async_unsubscribe_topics( + hass: HomeAssistant, sub_state: dict[str, EntitySubscription] | None +) -> dict[str, EntitySubscription]: """Unsubscribe from all MQTT topics managed by async_subscribe_topics.""" return await async_subscribe_topics(hass, sub_state, {}) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index f1e2cd0a4e1..f9410f66e8f 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -15,8 +15,8 @@ import voluptuous as vol from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.models import ( - Message as MQTTMessage, - PublishPayloadType, + ReceiveMessage as MQTTReceiveMessage, + ReceivePayloadType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -188,12 +188,12 @@ async def _get_gateway( mqtt.async_publish(topic, payload, qos, retain) def sub_callback( - topic: str, sub_cb: Callable[[str, PublishPayloadType, int], None], qos: int + topic: str, sub_cb: Callable[[str, ReceivePayloadType, int], None], qos: int ) -> None: """Call MQTT subscribe function.""" @callback - def internal_callback(msg: MQTTMessage) -> None: + def internal_callback(msg: MQTTReceiveMessage) -> None: """Call callback.""" sub_cb(msg.topic, msg.payload, msg.qos) diff --git a/tests/common.py b/tests/common.py index 03b53294db0..5de58a08472 100644 --- a/tests/common.py +++ b/tests/common.py @@ -34,7 +34,7 @@ from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, _async_get_device_automations as async_get_device_automations, ) -from homeassistant.components.mqtt.models import Message +from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config import async_process_component_config from homeassistant.const import ( DEVICE_DEFAULT_NAME, @@ -353,7 +353,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" if isinstance(payload, str): payload = payload.encode("utf-8") - msg = Message(topic, payload, qos, retain) + msg = ReceiveMessage(topic, payload, qos, retain) hass.data["mqtt"]._mqtt_handle_message(msg) diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 9f199f0aa66..767d6b9cfcf 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" from homeassistant import config_entries -from homeassistant.components.mqtt.models import Message +from homeassistant.components.mqtt.models import ReceiveMessage from tests.common import MockConfigEntry @@ -19,7 +19,9 @@ async def test_mqtt_abort_if_existing_entry(hass, mqtt_mock): async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): """Check MQTT flow aborts if discovery topic is invalid.""" - discovery_info = Message("", "", 0, False, subscribed_topic="custom_prefix/##") + discovery_info = ReceiveMessage( + "", "", 0, False, subscribed_topic="custom_prefix/##" + ) result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) @@ -29,7 +31,9 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): async def test_mqtt_setup(hass, mqtt_mock) -> None: """Test we can finish a config flow through MQTT with custom prefix.""" - discovery_info = Message("", "", 0, False, subscribed_topic="custom_prefix/123/#") + discovery_info = ReceiveMessage( + "", "", 0, False, subscribed_topic="custom_prefix/123/#" + ) result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) From 12082736a893dc1a9286a6e11a3fc3a8abfd8340 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Tue, 6 Jul 2021 14:55:34 +0200 Subject: [PATCH 115/818] Add type annotations to init and coordinator. Minor cleanups. (#52506) --- homeassistant/components/ezviz/__init__.py | 24 +++++++++---------- homeassistant/components/ezviz/coordinator.py | 17 +++++++------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 19dd5121d69..d2d98aa04cb 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1,10 +1,10 @@ """Support for Ezviz camera.""" -from datetime import timedelta import logging from pyezviz.client import EzvizClient from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_TIMEOUT, @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import ( @@ -28,8 +29,6 @@ from .coordinator import EzvizDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - PLATFORMS = [ "binary_sensor", "camera", @@ -38,17 +37,16 @@ PLATFORMS = [ ] -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ezviz from a config entry.""" hass.data.setdefault(DOMAIN, {}) if not entry.options: options = { - CONF_FFMPEG_ARGUMENTS: entry.data.get( - CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS - ), - CONF_TIMEOUT: entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + CONF_FFMPEG_ARGUMENTS: DEFAULT_FFMPEG_ARGUMENTS, + CONF_TIMEOUT: DEFAULT_TIMEOUT, } + hass.config_entries.async_update_entry(entry, options=options) if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: @@ -70,7 +68,9 @@ async def async_setup_entry(hass, entry): _LOGGER.error("Unable to connect to Ezviz service: %s", str(error)) raise ConfigEntryNotReady from error - coordinator = EzvizDataUpdateCoordinator(hass, api=ezviz_client) + coordinator = EzvizDataUpdateCoordinator( + hass, api=ezviz_client, api_timeout=entry.options[CONF_TIMEOUT] + ) await coordinator.async_refresh() if not coordinator.last_update_success: @@ -87,7 +87,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: @@ -100,12 +100,12 @@ async def async_unload_entry(hass, entry): return unload_ok -async def _async_update_listener(hass, entry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -def _get_ezviz_client_instance(entry): +def _get_ezviz_client_instance(entry: ConfigEntry) -> EzvizClient: """Initialize a new instance of EzvizClientApi.""" ezviz_client = EzvizClient( entry.data[CONF_USERNAME], diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index ad755edce12..8729aa4cf21 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -3,8 +3,10 @@ from datetime import timedelta import logging from async_timeout import timeout +from pyezviz.client import EzvizClient from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -15,23 +17,24 @@ _LOGGER = logging.getLogger(__name__) class EzvizDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Ezviz data.""" - def __init__(self, hass, *, api): + def __init__( + self, hass: HomeAssistant, *, api: EzvizClient, api_timeout: int + ) -> None: """Initialize global Ezviz data updater.""" self.ezviz_client = api + self._api_timeout = api_timeout update_interval = timedelta(seconds=30) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - def _update_data(self): + def _update_data(self) -> dict: """Fetch data from Ezviz via camera load function.""" - cameras = self.ezviz_client.load_cameras() + return self.ezviz_client.load_cameras() - return cameras - - async def _async_update_data(self): + async def _async_update_data(self) -> dict: """Fetch data from Ezviz.""" try: - async with timeout(35): + async with timeout(self._api_timeout): return await self.hass.async_add_executor_job(self._update_data) except (InvalidURL, HTTPError, PyEzvizError) as error: From a70dae0843994451b3b35e332816c55d241f58ff Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 6 Jul 2021 15:06:32 +0200 Subject: [PATCH 116/818] Enable strict typing for Fritz (#50668) Co-authored-by: Ruslan Sayfutdinov --- .strict-typing | 1 + homeassistant/components/fritz/common.py | 6 ++++- .../components/fritz/device_tracker.py | 4 +-- homeassistant/components/fritz/sensor.py | 26 +++++++++---------- homeassistant/components/fritz/switch.py | 2 +- mypy.ini | 11 ++++++++ 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/.strict-typing b/.strict-typing index 959aa702672..77d00928edd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -36,6 +36,7 @@ homeassistant.components.fitbit.* homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* homeassistant.components.frontend.* +homeassistant.components.fritz.* homeassistant.components.geo_location.* homeassistant.components.gios.* homeassistant.components.group.* diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 776c7a7a22e..cfa04338db1 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -111,6 +111,10 @@ class FritzBoxTools: timeout=60.0, ) + if not self.connection: + _LOGGER.error("Unable to establish a connection with %s", self.host) + return + self.fritz_status = FritzStatus(fc=self.connection) info = self.connection.call_action("DeviceInfo:1", "GetInfo") if not self._unique_id: @@ -189,7 +193,7 @@ class FritzBoxTools: def _update_info(self) -> list[HostInfo]: """Retrieve latest information from the FRITZ!Box.""" - return self.fritz_hosts.get_hosts_info() + return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return] def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index d4ff1dbd161..db6cfadcf5d 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -145,7 +145,7 @@ class FritzBoxTracker(ScannerEntity): def ip_address(self) -> str | None: """Return the primary ip address of the device.""" if self._mac: - return self._router.devices[self._mac].ip_address + return self._router.devices[self._mac].ip_address # type: ignore[no-any-return] return None @property @@ -157,7 +157,7 @@ class FritzBoxTracker(ScannerEntity): def hostname(self) -> str | None: """Return hostname of the device.""" if self._mac: - return self._router.devices[self._mac].hostname + return self._router.devices[self._mac].hostname # type: ignore[no-any-return] return None @property diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 042ec069864..5801bc752fe 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -39,37 +39,37 @@ def _retrieve_uptime_state(status: FritzStatus, last_value: str) -> str: def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: """Return external ip from device.""" - return status.external_ip + return status.external_ip # type: ignore[no-any-return] -def _retrieve_kib_s_sent_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_kib_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload transmission rate.""" - return round(status.transmission_rate[0] * 8 / 1024, 1) + return round(status.transmission_rate[0] * 8 / 1024, 1) # type: ignore[no-any-return] -def _retrieve_kib_s_received_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_kib_s_received_state(status: FritzStatus, last_value: str) -> float: """Return download transmission rate.""" - return round(status.transmission_rate[1] * 8 / 1024, 1) + return round(status.transmission_rate[1] * 8 / 1024, 1) # type: ignore[no-any-return] -def _retrieve_max_kib_s_sent_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_max_kib_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload max transmission rate.""" - return round(status.max_bit_rate[0] / 1024, 1) + return round(status.max_bit_rate[0] / 1024, 1) # type: ignore[no-any-return] -def _retrieve_max_kib_s_received_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_max_kib_s_received_state(status: FritzStatus, last_value: str) -> float: """Return download max transmission rate.""" - return round(status.max_bit_rate[1] / 1024, 1) + return round(status.max_bit_rate[1] / 1024, 1) # type: ignore[no-any-return] -def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload total data.""" - return round(status.bytes_sent * 8 / 1024 / 1024 / 1024, 1) + return round(status.bytes_sent * 8 / 1024 / 1024 / 1024, 1) # type: ignore[no-any-return] -def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> str: +def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> float: """Return download total data.""" - return round(status.bytes_received * 8 / 1024 / 1024 / 1024, 1) + return round(status.bytes_received * 8 / 1024 / 1024 / 1024, 1) # type: ignore[no-any-return] class SensorData(TypedDict, total=False): diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index d9690b64069..50e18302c9d 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -69,7 +69,7 @@ def service_call_action( return None try: - return fritzbox_tools.connection.call_action( + return fritzbox_tools.connection.call_action( # type: ignore[no-any-return] f"{service_name}:{service_suffix}", action_name, **kwargs, diff --git a/mypy.ini b/mypy.ini index a3c856f546c..cb3bf6ec698 100644 --- a/mypy.ini +++ b/mypy.ini @@ -407,6 +407,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fritz.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.geo_location.*] check_untyped_defs = true disallow_incomplete_defs = true From 046eb1690ac189a08dc74d89694737f9c8d5a820 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 6 Jul 2021 15:49:22 +0200 Subject: [PATCH 117/818] Fix Fritz Wi-Fi 6 networks with same name as other Wi-Fi (#52588) --- homeassistant/components/fritz/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 50e18302c9d..6e808598b77 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -245,7 +245,7 @@ def wifi_entities_list( ) -> list[FritzBoxWifiSwitch]: """Get list of wifi entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_WIFINETWORK) - std_table = {"ac": "5Ghz", "n": "2.4Ghz"} + std_table = {"ax": "Wifi6", "ac": "5Ghz", "n": "2.4Ghz"} networks: dict = {} for i in range(4): if not ("WLANConfiguration" + str(i)) in fritzbox_tools.connection.services: From 07bda0973e37fc7dd051f5f6e689d425f2aad6b2 Mon Sep 17 00:00:00 2001 From: ondras12345 Date: Tue, 6 Jul 2021 16:03:54 +0200 Subject: [PATCH 118/818] Fix update of Xiaomi Miio vacuum taking too long (#52539) Home assistant log would get spammed with messages like Update of vacuum.vacuum_name is taking over 10 seconds every 20 seconds if the vacuum was not reachable through the network. See #52353 --- homeassistant/components/xiaomi_miio/vacuum.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index d0bfc148594..cdd53e784b3 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -507,7 +507,11 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): # Fetch timers separately, see #38285 try: - self._timers = self._device.timer() + # Do not try this if the first fetch timed out. + # Two timeouts take longer than 10 seconds and trigger a warning. + # See #52353 + if self._available: + self._timers = self._device.timer() except DeviceException as exc: _LOGGER.debug( "Unable to fetch timers, this may happen on some devices: %s", exc From 6e779855f7f9aeb008c56230e4bb25db43243e75 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 6 Jul 2021 10:18:00 -0400 Subject: [PATCH 119/818] Clean up alarmdecoder (#52517) * Clean up alarmdecoder * fix * try again * tweak --- .../components/alarmdecoder/__init__.py | 2 +- .../alarmdecoder/alarm_control_panel.py | 108 +++++------------- .../components/alarmdecoder/binary_sensor.py | 63 ++++------ .../components/alarmdecoder/sensor.py | 35 +----- .../alarmdecoder/test_config_flow.py | 10 ++ 5 files changed, 65 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index aff7dd8c5ba..69870450869 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -129,7 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a AlarmDecoder entry.""" hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 47da48de66f..3a0b3923231 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -77,24 +77,18 @@ async def async_setup_entry( class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Representation of an AlarmDecoder-based alarm panel.""" + _attr_name = "Alarm Panel" + _attr_should_poll = False + _attr_code_format = FORMAT_NUMBER + _attr_supported_features = ( + SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + ) + def __init__(self, client, auto_bypass, code_arm_required, alt_night_mode): """Initialize the alarm panel.""" self._client = client - self._display = "" - self._name = "Alarm Panel" - self._state = None - self._ac_power = None - self._alarm_event_occurred = None - self._backlight_on = None - self._battery_low = None - self._check_zone = None - self._chime = None - self._entry_delay_off = None - self._programming_mode = None - self._ready = None - self._zone_bypassed = None self._auto_bypass = auto_bypass - self._code_arm_required = code_arm_required + self._attr_code_arm_required = code_arm_required self._alt_night_mode = alt_night_mode async def async_added_to_hass(self): @@ -108,75 +102,29 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): def _message_callback(self, message): """Handle received messages.""" if message.alarm_sounding or message.fire_alarm: - self._state = STATE_ALARM_TRIGGERED + self._attr_state = STATE_ALARM_TRIGGERED elif message.armed_away: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY elif message.armed_home and (message.entry_delay_off or message.perimeter_only): - self._state = STATE_ALARM_ARMED_NIGHT + self._attr_state = STATE_ALARM_ARMED_NIGHT elif message.armed_home: - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME else: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED - self._ac_power = message.ac_power - self._alarm_event_occurred = message.alarm_event_occurred - self._backlight_on = message.backlight_on - self._battery_low = message.battery_low - self._check_zone = message.check_zone - self._chime = message.chime_on - self._entry_delay_off = message.entry_delay_off - self._programming_mode = message.programming_mode - self._ready = message.ready - self._zone_bypassed = message.zone_bypassed - - self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def code_format(self): - """Return one or more digits/characters.""" - return FORMAT_NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT - - @property - def code_arm_required(self): - """Whether the code is required for arm actions.""" - return self._code_arm_required - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - "ac_power": self._ac_power, - "alarm_event_occurred": self._alarm_event_occurred, - "backlight_on": self._backlight_on, - "battery_low": self._battery_low, - "check_zone": self._check_zone, - "chime": self._chime, - "entry_delay_off": self._entry_delay_off, - "programming_mode": self._programming_mode, - "ready": self._ready, - "zone_bypassed": self._zone_bypassed, - "code_arm_required": self._code_arm_required, + self._attr_extra_state_attributes = { + "ac_power": message.ac_power, + "alarm_event_occurred": message.alarm_event_occurred, + "backlight_on": message.backlight_on, + "battery_low": message.battery_low, + "check_zone": message.check_zone, + "chime": message.chime_on, + "entry_delay_off": message.entry_delay_off, + "programming_mode": message.programming_mode, + "ready": message.ready, + "zone_bypassed": message.zone_bypassed, } + self.schedule_update_ha_state() def alarm_disarm(self, code=None): """Send disarm command.""" @@ -187,7 +135,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Send arm away command.""" self._client.arm_away( code=code, - code_arm_required=self._code_arm_required, + code_arm_required=self._attr_code_arm_required, auto_bypass=self._auto_bypass, ) @@ -195,7 +143,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Send arm home command.""" self._client.arm_home( code=code, - code_arm_required=self._code_arm_required, + code_arm_required=self._attr_code_arm_required, auto_bypass=self._auto_bypass, ) @@ -203,7 +151,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Send arm night command.""" self._client.arm_night( code=code, - code_arm_required=self._code_arm_required, + code_arm_required=self._attr_code_arm_required, alt_night_mode=self._alt_night_mode, auto_bypass=self._auto_bypass, ) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 71bcc399e08..397394e256b 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -60,6 +60,8 @@ async def async_setup_entry( class AlarmDecoderBinarySensor(BinarySensorEntity): """Representation of an AlarmDecoder binary sensor.""" + _attr_should_poll = False + def __init__( self, zone_number, @@ -73,13 +75,12 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): """Initialize the binary_sensor.""" self._zone_number = int(zone_number) self._zone_type = zone_type - self._state = None - self._name = zone_name + self._attr_name = zone_name self._rfid = zone_rfid self._loop = zone_loop - self._rfstate = None self._relay_addr = relay_addr self._relay_chan = relay_chan + self._attr_device_class = zone_type async def async_added_to_hass(self): """Register callbacks.""" @@ -107,59 +108,35 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): ) ) - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attr = {CONF_ZONE_NUMBER: self._zone_number} - if self._rfid and self._rfstate is not None: - attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01) - attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02) - attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04) - attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08) - attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10) - attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20) - attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40) - attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80) - return attr - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state == 1 - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._zone_type - def _fault_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: - self._state = 1 + self._attr_state = 1 self.schedule_update_ha_state() def _restore_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or (int(zone) == self._zone_number and not self._loop): - self._state = 0 + self._attr_state = 0 self.schedule_update_ha_state() def _rfx_message_callback(self, message): """Update RF state.""" if self._rfid and message and message.serial_number == self._rfid: - self._rfstate = message.value + rfstate = message.value if self._loop: - self._state = 1 if message.loop[self._loop - 1] else 0 + self._attr_state = 1 if message.loop[self._loop - 1] else 0 + attr = {CONF_ZONE_NUMBER: self._zone_number} + if self._rfid and rfstate is not None: + attr[ATTR_RF_BIT0] = bool(rfstate & 0x01) + attr[ATTR_RF_LOW_BAT] = bool(rfstate & 0x02) + attr[ATTR_RF_SUPERVISED] = bool(rfstate & 0x04) + attr[ATTR_RF_BIT3] = bool(rfstate & 0x08) + attr[ATTR_RF_LOOP3] = bool(rfstate & 0x10) + attr[ATTR_RF_LOOP2] = bool(rfstate & 0x20) + attr[ATTR_RF_LOOP4] = bool(rfstate & 0x40) + attr[ATTR_RF_LOOP1] = bool(rfstate & 0x80) + self._attr_extra_state_attributes = attr self.schedule_update_ha_state() def _rel_message_callback(self, message): @@ -173,5 +150,5 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): message.channel, message.value, ) - self._state = message.value + self._attr_state = message.value self.schedule_update_ha_state() diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index e3c85cb5893..67b7ee4861a 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -8,7 +8,7 @@ from .const import SIGNAL_PANEL_MESSAGE async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities -): +) -> bool: """Set up for AlarmDecoder sensor.""" entity = AlarmDecoderSensor() @@ -19,12 +19,9 @@ async def async_setup_entry( class AlarmDecoderSensor(SensorEntity): """Representation of an AlarmDecoder keypad.""" - def __init__(self): - """Initialize the alarm panel.""" - self._display = "" - self._state = None - self._icon = "mdi:alarm-check" - self._name = "Alarm Panel Display" + _attr_icon = "mdi:alarm-check" + _attr_name = "Alarm Panel Display" + _attr_should_poll = False async def async_added_to_hass(self): """Register callbacks.""" @@ -35,26 +32,6 @@ class AlarmDecoderSensor(SensorEntity): ) def _message_callback(self, message): - if self._display != message.text: - self._display = message.text + if self._attr_state != message.text: + self._attr_state = message.text self.schedule_update_ha_state() - - @property - def icon(self): - """Return the icon if any.""" - return self._icon - - @property - def state(self): - """Return the overall state.""" - return self._display - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 2ab965023bd..8a2aae48f9b 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -126,6 +126,16 @@ async def test_setup_connection_error(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + with patch( + "homeassistant.components.alarmdecoder.config_flow.AdExt.open", + side_effect=Exception, + ), patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], connection_settings + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + async def test_options_arm_flow(hass: HomeAssistant): """Test arm options flow.""" From 81fe3583c9b2a070fa301330529d61f08d91e5dd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Jul 2021 10:33:07 -0400 Subject: [PATCH 120/818] Don't raise when setting HVAC mode without a mode ZwaveValue (#52444) * Don't raise an error when setting HVAC mode without a value * change logic based on discord convo and add tests * tweak --- homeassistant/components/zwave_js/climate.py | 16 ++++++------- tests/components/zwave_js/test_climate.py | 24 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 4ef13276fbe..43363538500 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -469,15 +469,15 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if not self._current_mode: - # Thermostat(valve) with no support for setting a mode - raise ValueError( - f"Thermostat {self.entity_id} does not support setting a mode" - ) - hvac_mode_value = self._hvac_modes.get(hvac_mode) - if hvac_mode_value is None: + hvac_mode_id = self._hvac_modes.get(hvac_mode) + if hvac_mode_id is None: raise ValueError(f"Received an invalid hvac mode: {hvac_mode}") - await self.info.node.async_set_value(self._current_mode, hvac_mode_value) + + if not self._current_mode: + # Thermostat(valve) has no support for setting a mode, so we make it a no-op + return + + await self.info.node.async_set_value(self._current_mode, hvac_mode_id) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index f86052b3692..fefa680ce77 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -382,6 +382,30 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat blocking=True, ) + # Test setting illegal mode raises an error + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_COOL, + }, + blocking=True, + ) + + # Test that setting HVAC_MODE_HEAT works. If the no-op logic didn't work, this would + # raise an error + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_HEAT, + }, + blocking=True, + ) + assert len(client.async_send_command_no_wait.call_args_list) == 1 args = client.async_send_command_no_wait.call_args_list[0][0][0] assert args["command"] == "node.set_value" From 7ba1fdea70b7279a99c49ded1f0dbcbddcdf9cb9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Jul 2021 17:13:45 +0200 Subject: [PATCH 121/818] Bump hatasmota to 0.2.20 (#52591) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index f87e0c189f1..ae918c7fe44 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.19"], + "requirements": ["hatasmota==0.2.20"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index d1919653e57..9a3edf7ed4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ hass-nabucasa==0.44.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.19 +hatasmota==0.2.20 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5658bf9b5a..f5eb045dadf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ hangups==0.4.14 hass-nabucasa==0.44.0 # homeassistant.components.tasmota -hatasmota==0.2.19 +hatasmota==0.2.20 # homeassistant.components.jewish_calendar hdate==0.10.2 From 605f65b75df5240a920ce34c35a1192d8883369e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Jul 2021 17:18:54 +0200 Subject: [PATCH 122/818] Make use of entry id rather than unique id when storing deconz entry in hass.data (#52584) * Make use of entry id rather than unique id when storing entry in hass data * Update homeassistant/components/deconz/services.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/deconz/__init__.py | 7 ++- homeassistant/components/deconz/gateway.py | 4 +- homeassistant/components/deconz/services.py | 49 +++++++++++---------- tests/components/deconz/test_init.py | 10 ++--- tests/components/deconz/test_services.py | 23 +++++++++- 5 files changed, 56 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8b47363c7ba..1b9a418fb29 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -20,8 +20,7 @@ async def async_setup_entry(hass, config_entry): Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + hass.data.setdefault(DOMAIN, {}) await async_update_group_unique_id(hass, config_entry) @@ -33,7 +32,7 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False - hass.data[DOMAIN][config_entry.unique_id] = gateway + hass.data[DOMAIN][config_entry.entry_id] = gateway await gateway.async_update_device_registry() @@ -48,7 +47,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" - gateway = hass.data[DOMAIN].pop(config_entry.unique_id) + gateway = hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: await async_unload_services(hass) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8b057ab9e51..0a7d7e0c849 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -33,8 +33,8 @@ from .errors import AuthenticationRequired, CannotConnect @callback def get_gateway_from_config_entry(hass, config_entry): - """Return gateway with a matching bridge id.""" - return hass.data[DECONZ_DOMAIN][config_entry.unique_id] + """Return gateway with a matching config entry ID.""" + return hass.data[DECONZ_DOMAIN][config_entry.entry_id] class DeconzGateway: diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index d524354ff0b..a4f4aec6a76 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -59,14 +59,29 @@ async def async_setup_services(hass): service = service_call.service service_data = service_call.data + gateway = get_master_gateway(hass) + if CONF_BRIDGE_ID in service_data: + found_gateway = False + bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID]) + + for possible_gateway in hass.data[DOMAIN].values(): + if possible_gateway.bridgeid == bridge_id: + gateway = possible_gateway + found_gateway = True + break + + if not found_gateway: + LOGGER.error("Could not find the gateway %s", bridge_id) + return + if service == SERVICE_CONFIGURE_DEVICE: - await async_configure_service(hass, service_data) + await async_configure_service(gateway, service_data) elif service == SERVICE_DEVICE_REFRESH: - await async_refresh_devices_service(hass, service_data) + await async_refresh_devices_service(gateway) elif service == SERVICE_REMOVE_ORPHANED_ENTRIES: - await async_remove_orphaned_entries_service(hass, service_data) + await async_remove_orphaned_entries_service(gateway) hass.services.async_register( DOMAIN, @@ -102,7 +117,7 @@ async def async_unload_services(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES) -async def async_configure_service(hass, data): +async def async_configure_service(gateway, data): """Set attribute of device in deCONZ. Entity is used to resolve to a device path (e.g. '/lights/1'). @@ -118,10 +133,6 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - gateway = get_master_gateway(hass) - if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] @@ -136,31 +147,21 @@ async def async_configure_service(hass, data): await gateway.api.request("put", field, json=data) -async def async_refresh_devices_service(hass, data): +async def async_refresh_devices_service(gateway): """Refresh available devices from deCONZ.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - gateway.ignore_state_updates = True await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP, force=True) - gateway.async_add_device_callback(NEW_LIGHT, force=True) - gateway.async_add_device_callback(NEW_SCENE, force=True) - gateway.async_add_device_callback(NEW_SENSOR, force=True) + for new_device_type in [NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR]: + gateway.async_add_device_callback(new_device_type, force=True) -async def async_remove_orphaned_entries_service(hass, data): +async def async_remove_orphaned_entries_service(gateway): """Remove orphaned deCONZ entries from device and entity registries.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - device_registry, entity_registry = await asyncio.gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), + gateway.hass.helpers.device_registry.async_get_registry(), + gateway.hass.helpers.entity_registry.async_get_registry(), ) entity_entries = async_entries_for_config_entry( diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 6583372d7bd..814ec588b1e 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -61,8 +61,8 @@ async def test_setup_entry_successful(hass, aioclient_mock): config_entry = await setup_deconz_integration(hass, aioclient_mock) assert hass.data[DECONZ_DOMAIN] - assert config_entry.unique_id in hass.data[DECONZ_DOMAIN] - assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master + assert config_entry.entry_id in hass.data[DECONZ_DOMAIN] + assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master async def test_setup_entry_multiple_gateways(hass, aioclient_mock): @@ -80,8 +80,8 @@ async def test_setup_entry_multiple_gateways(hass, aioclient_mock): ) assert len(hass.data[DECONZ_DOMAIN]) == 2 - assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master - assert not hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master + assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master + assert not hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master async def test_unload_entry(hass, aioclient_mock): @@ -112,7 +112,7 @@ async def test_unload_entry_multiple_gateways(hass, aioclient_mock): assert await async_unload_entry(hass, config_entry) assert len(hass.data[DECONZ_DOMAIN]) == 1 - assert hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master + assert hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master async def test_update_group_unique_id(hass): diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 7ad9c82b08c..8a696da9eb4 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -152,8 +152,27 @@ async def test_configure_service_with_entity_and_field(hass, aioclient_mock): assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} +async def test_configure_service_with_faulty_bridgeid(hass, aioclient_mock): + """Test that service fails on a bad bridge id.""" + await setup_deconz_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() + + data = { + CONF_BRIDGE_ID: "Bad bridge id", + SERVICE_FIELD: "/lights/1", + SERVICE_DATA: {"on": True}, + } + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 0 + + async def test_configure_service_with_faulty_field(hass, aioclient_mock): - """Test that service invokes pydeconz with the correct path and data.""" + """Test that service fails on a bad field.""" await setup_deconz_integration(hass, aioclient_mock) data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}} @@ -166,7 +185,7 @@ async def test_configure_service_with_faulty_field(hass, aioclient_mock): async def test_configure_service_with_faulty_entity(hass, aioclient_mock): - """Test that service invokes pydeconz with the correct path and data.""" + """Test that service on a non existing entity.""" await setup_deconz_integration(hass, aioclient_mock) aioclient_mock.clear_requests() From 3d7fd83ad42a7947d6c96c51be74914a8334f9d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Jul 2021 17:35:11 +0200 Subject: [PATCH 123/818] Add home-assistant/core as codeowner for the template integration (#52592) --- CODEOWNERS | 2 +- homeassistant/components/template/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 46eb14dd66a..5555850f4d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -503,7 +503,7 @@ homeassistant/components/tapsaff/* @bazwilliams homeassistant/components/tasmota/* @emontnemery homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike -homeassistant/components/template/* @PhracturedBlue @tetienne +homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core homeassistant/components/tesla/* @zabuldon @alandtse homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/thethingsnetwork/* @fabaff diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index fe9edb21ea1..785088d2645 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -2,7 +2,7 @@ "domain": "template", "name": "Template", "documentation": "https://www.home-assistant.io/integrations/template", - "codeowners": ["@PhracturedBlue", "@tetienne"], + "codeowners": ["@PhracturedBlue", "@tetienne", "@home-assistant/core"], "quality_scale": "internal", "after_dependencies": ["group"], "iot_class": "local_push" From 62c3b3bdfea493938ef10ef44af7d53bad527ac6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 6 Jul 2021 17:52:22 +0200 Subject: [PATCH 124/818] Use HA location name as `name` in GIOS integration (#52585) --- homeassistant/components/gios/config_flow.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index b8a52254c32..d2bd3968d10 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -14,14 +14,7 @@ from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import API_TIMEOUT, CONF_STATION_ID, DEFAULT_NAME, DOMAIN - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_STATION_ID): int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, - } -) +from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -60,5 +53,14 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_STATION_ID] = "invalid_sensors_data" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_STATION_ID): int, + vol.Optional( + CONF_NAME, default=self.hass.config.location_name + ): str, + } + ), + errors=errors, ) From 8fce858a76588138a5d450555b964efe3379facc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Jul 2021 18:11:22 +0200 Subject: [PATCH 125/818] Upgrade numpy to 1.21.0 (#52586) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 38ee883e559..817623a6895 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.20.3"], + "requirements": ["numpy==1.21.0"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 75249ded6a1..84696f4e579 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.20.3", "pyiqvia==1.0.0"], + "requirements": ["numpy==1.21.0", "pyiqvia==1.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index d011c485d42..e5a82c69aaa 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.20.3", "opencv-python-headless==4.4.0.42"], + "requirements": ["numpy==1.21.0", "opencv-python-headless==4.4.0.42"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 1b161b4aec8..e16da5d2ab2 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.20.3", + "numpy==1.21.0", "pillow==8.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 508ce659a4a..75887e5e78f 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.20.3"], + "requirements": ["numpy==1.21.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 9a3edf7ed4c..3c68ba9c5de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1060,7 +1060,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.20.3 +numpy==1.21.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5eb045dadf..378846a01b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.20.3 +numpy==1.21.0 # homeassistant.components.google oauth2client==4.0.0 From 6c8de16fbc6ad20e1bfbc1f2f1b9837a3bccbe57 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Jul 2021 18:21:00 +0200 Subject: [PATCH 126/818] Enable basic type checking for fan (#52471) --- homeassistant/components/fan/__init__.py | 18 +++++++++++------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 20a11fd89f1..248e7d095d0 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -246,7 +246,7 @@ class FanEntity(ToggleEntity): await self.async_turn_off() return - if speed in self.preset_modes: + if self.preset_modes and speed in self.preset_modes: if not hasattr(self.async_set_preset_mode, _FAN_NATIVE): await self.async_set_preset_mode(speed) return @@ -375,7 +375,7 @@ class FanEntity(ToggleEntity): _LOGGER.warning( "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead" ) - if speed in self.preset_modes: + if self.preset_modes and speed in self.preset_modes: preset_mode = speed percentage = None else: @@ -463,9 +463,13 @@ class FanEntity(ToggleEntity): @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" - if not self._implemented_preset_mode and self.speed in self.preset_modes: + if ( + not self._implemented_preset_mode + and self.preset_modes + and self.speed in self.preset_modes + ): return None - if not self._implemented_percentage: + if self.speed is not None and not self._implemented_percentage: return self.speed_to_percentage(self.speed) return 0 @@ -488,7 +492,7 @@ class FanEntity(ToggleEntity): speeds = [] if self._implemented_percentage: speeds += [SPEED_OFF, *LEGACY_SPEED_LIST] - if self._implemented_preset_mode: + if self._implemented_preset_mode and self.preset_modes: speeds += self.preset_modes return speeds @@ -594,7 +598,7 @@ class FanEntity(ToggleEntity): @property def state_attributes(self) -> dict: """Return optional state attributes.""" - data = {} + data: dict[str, float | str | None] = {} supported_features = self.supported_features if supported_features & SUPPORT_DIRECTION: @@ -628,7 +632,7 @@ class FanEntity(ToggleEntity): Requires SUPPORT_SET_SPEED. """ speed = self.speed - if speed in self.preset_modes: + if self.preset_modes and speed in self.preset_modes: return speed return None diff --git a/mypy.ini b/mypy.ini index cb3bf6ec698..4c693dcc5ee 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1159,9 +1159,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.*] ignore_errors = true -[mypy-homeassistant.components.fan.*] -ignore_errors = true - [mypy-homeassistant.components.filter.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d234f96589a..1873ebd46c1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -52,7 +52,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.entur_public_transport.*", "homeassistant.components.esphome.*", "homeassistant.components.evohome.*", - "homeassistant.components.fan.*", "homeassistant.components.filter.*", "homeassistant.components.fints.*", "homeassistant.components.fireservicerota.*", From 40ad25df4c5a1c4817ec9f71c315369f6e2f468e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 6 Jul 2021 11:21:25 -0500 Subject: [PATCH 127/818] Fresh attempt at SimpliSafe auto-relogin (#52567) * Fresh attempt at SimpliSafe auto-relogin * Fix tests --- .../components/simplisafe/__init__.py | 62 +++++-------------- .../components/simplisafe/config_flow.py | 17 ++--- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/strings.json | 2 +- .../simplisafe/translations/en.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/simplisafe/test_config_flow.py | 43 ++++++------- 8 files changed, 52 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c4856788848..14fc4bf9a5a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -2,11 +2,11 @@ import asyncio from uuid import UUID -from simplipy import API +from simplipy import get_api from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -105,14 +105,6 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -@callback -def _async_save_refresh_token(hass, config_entry, token): - """Save a refresh token to the config entry.""" - hass.config_entries.async_update_entry( - config_entry, data={**config_entry.data, CONF_TOKEN: token} - ) - - async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -139,6 +131,9 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] + if CONF_PASSWORD not in config_entry.data: + raise ConfigEntryAuthFailed("Config schema change requires re-authentication") + entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -161,19 +156,19 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 websession = aiohttp_client.async_get_clientsession(hass) try: - api = await API.login_via_token( - config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + api = await get_api( + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + client_id=client_id, + session=websession, ) - except InvalidCredentialsError: - LOGGER.error("Invalid credentials provided") - return False + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - _async_save_refresh_token(hass, config_entry, api.refresh_token) - - simplisafe = SimpliSafe(hass, api, config_entry) + simplisafe = SimpliSafe(hass, config_entry, api) try: await simplisafe.async_init() @@ -296,10 +291,9 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, api, config_entry): + def __init__(self, hass, config_entry, api): """Initialize.""" self._api = api - self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -376,23 +370,7 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - if self._emergency_refresh_token_used: - raise ConfigEntryAuthFailed( - "Update failed with stored refresh token" - ) - - LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") - self._emergency_refresh_token_used = True - - try: - await self._api.refresh_access_token( - self.config_entry.data[CONF_TOKEN] - ) - return - except SimplipyError as err: - raise UpdateFailed( # pylint: disable=raise-missing-from - f"Error while using stored refresh token: {err}" - ) + raise ConfigEntryAuthFailed("Invalid credentials") from result if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -403,16 +381,6 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") - if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: - _async_save_refresh_token( - self._hass, self.config_entry, self._api.refresh_token - ) - - # If we've reached this point using an emergency refresh token, we're in the - # clear and we can discard it: - if self._emergency_refresh_token_used: - self._emergency_refresh_token_used = False - class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index ba51356f770..ac31779175f 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the SimpliSafe component.""" -from simplipy import API +from simplipy import get_api from simplipy.errors import ( InvalidCredentialsError, PendingAuthorizationError, @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -47,7 +47,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): client_id = await async_get_client_id(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass) - return await API.login_via_credentials( + return await get_api( self._username, self._password, client_id=client_id, @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) @@ -89,6 +89,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -98,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -108,7 +111,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 79e11828eaa..eff37bf1548 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==10.0.0"], + "requirements": ["simplisafe-python==11.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index ad973261a0e..23f85495025 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index b9e274666bb..331eb65ca83 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/requirements_all.txt b/requirements_all.txt index 3c68ba9c5de..0e7db016e7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2105,7 +2105,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==10.0.0 +simplisafe-python==11.0.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 378846a01b1..d5c89aa1fb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1154,7 +1154,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==10.0.0 +simplisafe-python==11.0.0 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a048e4b0745..4d438965806 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the SimpliSafe config flow.""" -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, patch from simplipy.errors import ( InvalidCredentialsError, @@ -10,18 +10,11 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry -def mock_api(): - """Mock SimpliSafe API class.""" - api = MagicMock() - type(api).refresh_token = PropertyMock(return_value="12345abc") - return api - - async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" conf = { @@ -33,7 +26,11 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -49,7 +46,7 @@ async def test_invalid_credentials(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=InvalidCredentialsError), ): result = await hass.config_entries.flow.async_init( @@ -102,7 +99,11 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -118,8 +119,8 @@ async def test_step_reauth(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ), patch("homeassistant.components.simplisafe.config_flow.get_api"), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -141,7 +142,7 @@ async def test_step_user(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -151,7 +152,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -165,7 +166,7 @@ async def test_step_user_mfa(hass): } with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=PendingAuthorizationError), ): result = await hass.config_entries.flow.async_init( @@ -174,7 +175,7 @@ async def test_step_user_mfa(hass): assert result["step_id"] == "mfa" with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=PendingAuthorizationError), ): # Simulate the user pressing the MFA submit button without having clicked @@ -187,7 +188,7 @@ async def test_step_user_mfa(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -197,7 +198,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -207,7 +208,7 @@ async def test_unknown_error(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=SimplipyError), ): result = await hass.config_entries.flow.async_init( From e1e3f68d0b606930bd98834caf0ac524acb75f73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jul 2021 11:28:23 -0500 Subject: [PATCH 128/818] Revert nmap_tracker to 2021.6 version (#52573) * Revert nmap_tracker to 2021.6 version - Its unlikely we will be able to solve #52565 before release * hassfest --- .coveragerc | 3 +- CODEOWNERS | 1 - .../components/nmap_tracker/__init__.py | 396 +----------------- .../components/nmap_tracker/config_flow.py | 223 ---------- .../components/nmap_tracker/device_tracker.py | 271 +++++------- .../components/nmap_tracker/manifest.json | 12 +- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 10 +- requirements_test_all.txt | 7 - tests/components/nmap_tracker/__init__.py | 1 - .../nmap_tracker/test_config_flow.py | 310 -------------- 11 files changed, 106 insertions(+), 1129 deletions(-) delete mode 100644 homeassistant/components/nmap_tracker/config_flow.py delete mode 100644 tests/components/nmap_tracker/__init__.py delete mode 100644 tests/components/nmap_tracker/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 74797f83087..8ba0356fa9c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -690,8 +690,7 @@ omit = homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* - homeassistant/components/nmap_tracker/__init__.py - homeassistant/components/nmap_tracker/device_tracker.py + homeassistant/components/nmap_tracker/* homeassistant/components/nmbs/sensor.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 5555850f4d0..c420d297afa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,7 +332,6 @@ homeassistant/components/nextcloud/* @meichthys homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole -homeassistant/components/nmap_tracker/* @bdraco homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff homeassistant/components/noaa_tides/* @jdelaney72 diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 76a7e44f153..da699caaa73 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -1,395 +1 @@ -"""The Nmap Tracker integration.""" -from __future__ import annotations - -import asyncio -import contextlib -from dataclasses import dataclass -from datetime import datetime, timedelta -import logging - -import aiohttp -from getmac import get_mac_address -from mac_vendor_lookup import AsyncMacLookup -from nmap import PortScanner, PortScannerError - -from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.dt as dt_util - -from .const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_TRACK_NEW_DEVICES, - DOMAIN, - NMAP_TRACKED_DEVICES, - PLATFORMS, - TRACKER_SCAN_INTERVAL, -) - -# Some version of nmap will fail with 'Assertion failed: htn.toclock_running == true (Target.cc: stopTimeOutClock: 503)\n' -NMAP_TRANSIENT_FAILURE = "Assertion failed: htn.toclock_running == true" -MAX_SCAN_ATTEMPTS = 16 -OFFLINE_SCANS_TO_MARK_UNAVAILABLE = 3 - - -def short_hostname(hostname): - """Return the first part of the hostname.""" - if hostname is None: - return None - return hostname.split(".")[0] - - -def human_readable_name(hostname, vendor, mac_address): - """Generate a human readable name.""" - if hostname: - return short_hostname(hostname) - if vendor: - return f"{vendor} {mac_address[-8:]}" - return f"Nmap Tracker {mac_address}" - - -@dataclass -class NmapDevice: - """Class for keeping track of an nmap tracked device.""" - - mac_address: str - hostname: str - name: str - ipv4: str - manufacturer: str - reason: str - last_update: datetime.datetime - offline_scans: int - - -class NmapTrackedDevices: - """Storage class for all nmap trackers.""" - - def __init__(self) -> None: - """Initialize the data.""" - self.tracked: dict = {} - self.ipv4_last_mac: dict = {} - self.config_entry_owner: dict = {} - - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Nmap Tracker from a config entry.""" - domain_data = hass.data.setdefault(DOMAIN, {}) - devices = domain_data.setdefault(NMAP_TRACKED_DEVICES, NmapTrackedDevices()) - scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) - await scanner.async_setup() - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - return True - - -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - _async_untrack_devices(hass, entry) - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - - -@callback -def _async_untrack_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Remove tracking for devices owned by this config entry.""" - devices = hass.data[DOMAIN][NMAP_TRACKED_DEVICES] - remove_mac_addresses = [ - mac_address - for mac_address, entry_id in devices.config_entry_owner.items() - if entry_id == entry.entry_id - ] - for mac_address in remove_mac_addresses: - if device := devices.tracked.pop(mac_address, None): - devices.ipv4_last_mac.pop(device.ipv4, None) - del devices.config_entry_owner[mac_address] - - -def signal_device_update(mac_address) -> str: - """Signal specific per nmap tracker entry to signal updates in device.""" - return f"{DOMAIN}-device-update-{mac_address}" - - -class NmapDeviceScanner: - """This class scans for devices using nmap.""" - - def __init__(self, hass, entry, devices): - """Initialize the scanner.""" - self.devices = devices - self.home_interval = None - - self._hass = hass - self._entry = entry - - self._scan_lock = None - self._stopping = False - self._scanner = None - - self._entry_id = entry.entry_id - self._hosts = None - self._options = None - self._exclude = None - self._scan_interval = None - self._track_new_devices = None - - self._known_mac_addresses = {} - self._finished_first_scan = False - self._last_results = [] - self._mac_vendor_lookup = None - - async def async_setup(self): - """Set up the tracker.""" - config = self._entry.options - self._track_new_devices = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES) - self._scan_interval = timedelta( - seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) - ) - hosts_list = cv.ensure_list_csv(config[CONF_HOSTS]) - self._hosts = [host for host in hosts_list if host != ""] - excludes_list = cv.ensure_list_csv(config[CONF_EXCLUDE]) - self._exclude = [exclude for exclude in excludes_list if exclude != ""] - self._options = config[CONF_OPTIONS] - self.home_interval = timedelta( - minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) - ) - self._scan_lock = asyncio.Lock() - if self._hass.state == CoreState.running: - await self._async_start_scanner() - return - - self._entry.async_on_unload( - self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner - ) - ) - registry = er.async_get(self._hass) - self._known_mac_addresses = { - entry.unique_id: entry.original_name - for entry in registry.entities.values() - if entry.config_entry_id == self._entry_id - } - - @property - def signal_device_new(self) -> str: - """Signal specific per nmap tracker entry to signal new device.""" - return f"{DOMAIN}-device-new-{self._entry_id}" - - @property - def signal_device_missing(self) -> str: - """Signal specific per nmap tracker entry to signal a missing device.""" - return f"{DOMAIN}-device-missing-{self._entry_id}" - - @callback - def _async_get_vendor(self, mac_address): - """Lookup the vendor.""" - oui = self._mac_vendor_lookup.sanitise(mac_address)[:6] - return self._mac_vendor_lookup.prefixes.get(oui) - - @callback - def _async_stop(self): - """Stop the scanner.""" - self._stopping = True - - async def _async_start_scanner(self, *_): - """Start the scanner.""" - self._entry.async_on_unload(self._async_stop) - self._entry.async_on_unload( - async_track_time_interval( - self._hass, - self._async_scan_devices, - self._scan_interval, - ) - ) - self._mac_vendor_lookup = AsyncMacLookup() - with contextlib.suppress((asyncio.TimeoutError, aiohttp.ClientError)): - # We don't care of this fails since its only - # improves the data when we don't have it from nmap - await self._mac_vendor_lookup.load_vendors() - self._hass.async_create_task(self._async_scan_devices()) - - def _build_options(self): - """Build the command line and strip out last results that do not need to be updated.""" - options = self._options - if self.home_interval: - boundary = dt_util.now() - self.home_interval - last_results = [ - device for device in self._last_results if device.last_update > boundary - ] - if last_results: - exclude_hosts = self._exclude + [device.ipv4 for device in last_results] - else: - exclude_hosts = self._exclude - else: - last_results = [] - exclude_hosts = self._exclude - if exclude_hosts: - options += f" --exclude {','.join(exclude_hosts)}" - # Report reason - if "--reason" not in options: - options += " --reason" - # Report down hosts - if "-v" not in options: - options += " -v" - self._last_results = last_results - return options - - async def _async_scan_devices(self, *_): - """Scan devices and dispatch.""" - if self._scan_lock.locked(): - _LOGGER.debug( - "Nmap scanning is taking longer than the scheduled interval: %s", - TRACKER_SCAN_INTERVAL, - ) - return - - async with self._scan_lock: - try: - await self._async_run_nmap_scan() - except PortScannerError as ex: - _LOGGER.error("Nmap scanning failed: %s", ex) - - if not self._finished_first_scan: - self._finished_first_scan = True - await self._async_mark_missing_devices_as_not_home() - - async def _async_mark_missing_devices_as_not_home(self): - # After all config entries have finished their first - # scan we mark devices that were not found as not_home - # from unavailable - now = dt_util.now() - for mac_address, original_name in self._known_mac_addresses.items(): - if mac_address in self.devices.tracked: - continue - self.devices.config_entry_owner[mac_address] = self._entry_id - self.devices.tracked[mac_address] = NmapDevice( - mac_address, - None, - original_name, - None, - self._async_get_vendor(mac_address), - "Device not found in initial scan", - now, - 1, - ) - async_dispatcher_send(self._hass, self.signal_device_missing, mac_address) - - def _run_nmap_scan(self): - """Run nmap and return the result.""" - options = self._build_options() - if not self._scanner: - self._scanner = PortScanner() - _LOGGER.debug("Scanning %s with args: %s", self._hosts, options) - for attempt in range(MAX_SCAN_ATTEMPTS): - try: - result = self._scanner.scan( - hosts=" ".join(self._hosts), - arguments=options, - timeout=TRACKER_SCAN_INTERVAL * 10, - ) - break - except PortScannerError as ex: - if attempt < (MAX_SCAN_ATTEMPTS - 1) and NMAP_TRANSIENT_FAILURE in str( - ex - ): - _LOGGER.debug("Nmap saw transient error %s", NMAP_TRANSIENT_FAILURE) - continue - raise - _LOGGER.debug( - "Finished scanning %s with args: %s", - self._hosts, - options, - ) - return result - - @callback - def _async_increment_device_offline(self, ipv4, reason): - """Mark an IP offline.""" - if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)): - return - if not (device := self.devices.tracked.get(formatted_mac)): - # Device was unloaded - return - device.offline_scans += 1 - if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE: - return - device.reason = reason - async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False) - del self.devices.ipv4_last_mac[ipv4] - - async def _async_run_nmap_scan(self): - """Scan the network for devices and dispatch events.""" - result = await self._hass.async_add_executor_job(self._run_nmap_scan) - if self._stopping: - return - - devices = self.devices - entry_id = self._entry_id - now = dt_util.now() - for ipv4, info in result["scan"].items(): - status = info["status"] - reason = status["reason"] - if status["state"] != "up": - self._async_increment_device_offline(ipv4, reason) - continue - # Mac address only returned if nmap ran as root - mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) - if mac is None: - self._async_increment_device_offline(ipv4, "No MAC address found") - _LOGGER.info("No MAC address found for %s", ipv4) - continue - - formatted_mac = format_mac(mac) - new = formatted_mac not in devices.tracked - if ( - new - and not self._track_new_devices - and formatted_mac not in devices.tracked - and formatted_mac not in self._known_mac_addresses - ): - continue - - if ( - devices.config_entry_owner.setdefault(formatted_mac, entry_id) - != entry_id - ): - continue - - hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) - name = human_readable_name(hostname, vendor, mac) - device = NmapDevice( - formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 - ) - - devices.tracked[formatted_mac] = device - devices.ipv4_last_mac[ipv4] = formatted_mac - self._last_results.append(device) - - if new: - async_dispatcher_send(self._hass, self.signal_device_new, formatted_mac) - else: - async_dispatcher_send( - self._hass, signal_device_update(formatted_mac), True - ) +"""The nmap_tracker component.""" diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py deleted file mode 100644 index 68e61745b63..00000000000 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Config flow for Nmap Tracker integration.""" -from __future__ import annotations - -from ipaddress import ip_address, ip_network, summarize_address_range -from typing import Any - -import ifaddr -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv -from homeassistant.util import get_local_ip - -from .const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_OPTIONS, - DEFAULT_TRACK_NEW_DEVICES, - DOMAIN, - TRACKER_SCAN_INTERVAL, -) - -DEFAULT_NETWORK_PREFIX = 24 - - -def get_network(): - """Search adapters for the network.""" - adapters = ifaddr.get_adapters() - local_ip = get_local_ip() - network_prefix = ( - get_ip_prefix_from_adapters(local_ip, adapters) or DEFAULT_NETWORK_PREFIX - ) - return str(ip_network(f"{local_ip}/{network_prefix}", False)) - - -def get_ip_prefix_from_adapters(local_ip, adapters): - """Find the network prefix for an adapter.""" - for adapter in adapters: - for ip_cfg in adapter.ips: - if local_ip == ip_cfg.ip: - return ip_cfg.network_prefix - - -def _normalize_ips_and_network(hosts_str): - """Check if a list of hosts are all ips or ip networks.""" - - normalized_hosts = [] - hosts = [host for host in cv.ensure_list_csv(hosts_str) if host != ""] - - for host in sorted(hosts): - try: - start, end = host.split("-", 1) - if "." not in end: - ip_1, ip_2, ip_3, _ = start.split(".", 3) - end = ".".join([ip_1, ip_2, ip_3, end]) - summarize_address_range(ip_address(start), ip_address(end)) - except ValueError: - pass - else: - normalized_hosts.append(host) - continue - - try: - ip_addr = ip_address(host) - except ValueError: - pass - else: - normalized_hosts.append(str(ip_addr)) - continue - - try: - network = ip_network(host) - except ValueError: - return None - else: - normalized_hosts.append(str(network)) - - return normalized_hosts - - -def normalize_input(user_input): - """Validate hosts and exclude are valid.""" - errors = {} - normalized_hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) - if not normalized_hosts: - errors[CONF_HOSTS] = "invalid_hosts" - else: - user_input[CONF_HOSTS] = ",".join(normalized_hosts) - - normalized_exclude = _normalize_ips_and_network(user_input[CONF_EXCLUDE]) - if normalized_exclude is None: - errors[CONF_EXCLUDE] = "invalid_hosts" - else: - user_input[CONF_EXCLUDE] = ",".join(normalized_exclude) - - return errors - - -async def _async_build_schema_with_user_input(hass, user_input, include_options): - hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) - exclude = user_input.get( - CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) - ) - schema = { - vol.Required(CONF_HOSTS, default=hosts): str, - vol.Required( - CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) - ): int, - vol.Optional(CONF_EXCLUDE, default=exclude): str, - vol.Optional( - CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) - ): str, - } - if include_options: - schema.update( - { - vol.Optional( - CONF_TRACK_NEW, - default=user_input.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES), - ): bool, - vol.Optional( - CONF_SCAN_INTERVAL, - default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL), - ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), - } - ) - return vol.Schema(schema) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for homekit.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.options = dict(config_entry.options) - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - errors = {} - if user_input is not None: - errors = normalize_input(user_input) - self.options.update(user_input) - - if not errors: - return self.async_create_entry( - title=f"Nmap Tracker {self.options[CONF_HOSTS]}", data=self.options - ) - - return self.async_show_form( - step_id="init", - data_schema=await _async_build_schema_with_user_input( - self.hass, self.options, True - ), - errors=errors, - ) - - -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Nmap Tracker.""" - - VERSION = 1 - - def __init__(self): - """Initialize config flow.""" - self.options = {} - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step.""" - errors = {} - if user_input is not None: - if not self._async_is_unique_host_list(user_input): - return self.async_abort(reason="already_configured") - - errors = normalize_input(user_input) - self.options.update(user_input) - - if not errors: - return self.async_create_entry( - title=f"Nmap Tracker {user_input[CONF_HOSTS]}", - data={}, - options=user_input, - ) - - return self.async_show_form( - step_id="user", - data_schema=await _async_build_schema_with_user_input( - self.hass, self.options, False - ), - errors=errors, - ) - - def _async_is_unique_host_list(self, user_input): - hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) - for entry in self._async_current_entries(): - if _normalize_ips_and_network(entry.options[CONF_HOSTS]) == hosts: - return False - return True - - async def async_step_import(self, user_input=None): - """Handle import from yaml.""" - if not self._async_is_unique_host_list(user_input): - return self.async_abort(reason="already_configured") - - normalize_input(user_input) - - return self.async_create_entry( - title=f"Nmap Tracker {user_input[CONF_HOSTS]}", data={}, options=user_input - ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 350e75adf48..69c65873e51 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,40 +1,29 @@ """Support for scanning a network with nmap.""" - +from collections import namedtuple +from datetime import timedelta import logging -from typing import Callable +from getmac import get_mac_address +from nmap import PortScanner, PortScannerError import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, - PLATFORM_SCHEMA, - SOURCE_TYPE_ROUTER, -) -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import ( - CONF_NEW_DEVICE_DEFAULTS, - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import NmapDeviceScanner, short_hostname, signal_device_update -from .const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_OPTIONS, - DEFAULT_TRACK_NEW_DEVICES, DOMAIN, - TRACKER_SCAN_INTERVAL, + PLATFORM_SCHEMA, + DeviceScanner, ) +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +# Interval in minutes to exclude devices from a scan while they are home +CONF_HOME_INTERVAL = "home_interval" +CONF_OPTIONS = "scan_options" +DEFAULT_OPTIONS = "-F --host-timeout 5s" + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOSTS): cv.ensure_list, @@ -45,164 +34,100 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_scanner(hass, config): +def get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" - validated_config = config[DEVICE_TRACKER_DOMAIN] + return NmapDeviceScanner(config[DOMAIN]) - if CONF_SCAN_INTERVAL in validated_config: - scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds() - else: - scan_interval = TRACKER_SCAN_INTERVAL - import_config = { - CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), - CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], - CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), - CONF_OPTIONS: validated_config[CONF_OPTIONS], - CONF_SCAN_INTERVAL: scan_interval, - CONF_TRACK_NEW: validated_config.get(CONF_NEW_DEVICE_DEFAULTS, {}).get( - CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES - ), - } +Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=import_config, + +class NmapDeviceScanner(DeviceScanner): + """This class scans for devices using nmap.""" + + exclude = [] + + def __init__(self, config): + """Initialize the scanner.""" + self.last_results = [] + + self.hosts = config[CONF_HOSTS] + self.exclude = config[CONF_EXCLUDE] + minutes = config[CONF_HOME_INTERVAL] + self._options = config[CONF_OPTIONS] + self.home_interval = timedelta(minutes=minutes) + + _LOGGER.debug("Scanner initialized") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + _LOGGER.debug("Nmap last results %s", self.last_results) + + return [device.mac for device in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + filter_named = [ + result.name for result in self.last_results if result.mac == device + ] + + if filter_named: + return filter_named[0] + return None + + def get_extra_attributes(self, device): + """Return the IP of the given device.""" + filter_ip = next( + (result.ip for result in self.last_results if result.mac == device), None ) - ) + return {"ip": filter_ip} - _LOGGER.warning( - "Your Nmap Tracker configuration has been imported into the UI, " - "please remove it from configuration.yaml. " - ) + def _update_info(self): + """Scan the network for devices. + Returns boolean if scanning successful. + """ + _LOGGER.debug("Scanning") -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable -) -> None: - """Set up device tracker for Nmap Tracker component.""" - nmap_tracker = hass.data[DOMAIN][entry.entry_id] + scanner = PortScanner() - @callback - def device_new(mac_address): - """Signal a new device.""" - async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, True)]) + options = self._options - @callback - def device_missing(mac_address): - """Signal a missing device.""" - async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, False)]) + if self.home_interval: + boundary = dt_util.now() - self.home_interval + last_results = [ + device for device in self.last_results if device.last_update > boundary + ] + if last_results: + exclude_hosts = self.exclude + [device.ip for device in last_results] + else: + exclude_hosts = self.exclude + else: + last_results = [] + exclude_hosts = self.exclude + if exclude_hosts: + options += f" --exclude {','.join(exclude_hosts)}" - entry.async_on_unload( - async_dispatcher_connect(hass, nmap_tracker.signal_device_new, device_new) - ) - entry.async_on_unload( - async_dispatcher_connect( - hass, nmap_tracker.signal_device_missing, device_missing - ) - ) + try: + result = scanner.scan(hosts=" ".join(self.hosts), arguments=options) + except PortScannerError: + return False + now = dt_util.now() + for ipv4, info in result["scan"].items(): + if info["status"]["state"] != "up": + continue + name = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 + # Mac address only returned if nmap ran as root + mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) + if mac is None: + _LOGGER.info("No MAC address found for %s", ipv4) + continue + last_results.append(Device(mac.upper(), name, ipv4, now)) -class NmapTrackerEntity(ScannerEntity): - """An Nmap Tracker entity.""" + self.last_results = last_results - def __init__( - self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool - ) -> None: - """Initialize an nmap tracker entity.""" - self._mac_address = mac_address - self._nmap_tracker = nmap_tracker - self._tracked = self._nmap_tracker.devices.tracked - self._active = active - - @property - def _device(self) -> bool: - """Get latest device state.""" - return self._tracked[self._mac_address] - - @property - def is_connected(self) -> bool: - """Return device status.""" - return self._active - - @property - def name(self) -> str: - """Return device name.""" - return self._device.name - - @property - def unique_id(self) -> str: - """Return device unique id.""" - return self._mac_address - - @property - def ip_address(self) -> str: - """Return the primary ip address of the device.""" - return self._device.ipv4 - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self._mac_address - - @property - def hostname(self) -> str: - """Return hostname of the device.""" - return short_hostname(self._device.hostname) - - @property - def source_type(self) -> str: - """Return tracker source type.""" - return SOURCE_TYPE_ROUTER - - @property - def device_info(self): - """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac_address)}, - "default_manufacturer": self._device.manufacturer, - "default_name": self.name, - } - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - @property - def icon(self): - """Return device icon.""" - return "mdi:lan-connect" if self._active else "mdi:lan-disconnect" - - @callback - def async_process_update(self, online: bool) -> None: - """Update device.""" - self._active = online - - @property - def extra_state_attributes(self): - """Return the attributes.""" - return { - "last_time_reachable": self._device.last_update.isoformat( - timespec="seconds" - ), - "reason": self._device.reason, - } - - @callback - def async_on_demand_update(self, online: bool): - """Update state.""" - self.async_process_update(online) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Register state update callback.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - signal_device_update(self._mac_address), - self.async_on_demand_update, - ) - ) + _LOGGER.debug("nmap scan successful") + return True diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index ee05843c4fe..9f81c0facaf 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -2,13 +2,7 @@ "domain": "nmap_tracker", "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", - "requirements": [ - "netmap==0.7.0.2", - "getmac==0.8.2", - "ifaddr==0.1.7", - "mac-vendor-lookup==0.1.11" - ], - "codeowners": ["@bdraco"], - "iot_class": "local_polling", - "config_flow": true + "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index aa6d9009574..e71503ce5fc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -176,7 +176,6 @@ FLOWS = [ "netatmo", "nexia", "nightscout", - "nmap_tracker", "notion", "nuheat", "nuki", diff --git a/requirements_all.txt b/requirements_all.txt index 0e7db016e7c..80b97205115 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -833,7 +833,6 @@ ibmiotf==0.3.4 icmplib==3.0 # homeassistant.components.network -# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.iglo @@ -935,9 +934,6 @@ lw12==0.9.2 # homeassistant.components.lyft lyft_rides==0.2 -# homeassistant.components.nmap_tracker -mac-vendor-lookup==0.1.11 - # homeassistant.components.magicseaweed magicseaweed==1.0.3 @@ -1016,9 +1012,6 @@ netdata==0.2.0 # homeassistant.components.discovery netdisco==2.9.0 -# homeassistant.components.nmap_tracker -netmap==0.7.0.2 - # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -1868,6 +1861,9 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.nmap_tracker +python-nmap==0.6.1 + # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5c89aa1fb9..306c158d627 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -480,7 +480,6 @@ iaqualink==0.3.90 icmplib==3.0 # homeassistant.components.network -# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.influxdb @@ -522,9 +521,6 @@ logi_circle==0.2.2 # homeassistant.components.luftdaten luftdaten==0.6.5 -# homeassistant.components.nmap_tracker -mac-vendor-lookup==0.1.11 - # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -573,9 +569,6 @@ nessclient==0.9.15 # homeassistant.components.discovery netdisco==2.9.0 -# homeassistant.components.nmap_tracker -netmap==0.7.0.2 - # homeassistant.components.nam nettigo-air-monitor==1.0.0 diff --git a/tests/components/nmap_tracker/__init__.py b/tests/components/nmap_tracker/__init__.py deleted file mode 100644 index f5e0c85df31..00000000000 --- a/tests/components/nmap_tracker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Nmap Tracker integration.""" diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py deleted file mode 100644 index c4e82936b88..00000000000 --- a/tests/components/nmap_tracker/test_config_flow.py +++ /dev/null @@ -1,310 +0,0 @@ -"""Test the Nmap Tracker config flow.""" -from unittest.mock import patch - -import pytest - -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.components.nmap_tracker.const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_OPTIONS, - DOMAIN, -) -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS -from homeassistant.core import CoreState, HomeAssistant - -from tests.common import MockConfigEntry - - -@pytest.mark.parametrize( - "hosts", ["1.1.1.1", "192.168.1.0/24", "192.168.1.0/24,192.168.2.0/24"] -) -async def test_form(hass: HomeAssistant, hosts: str) -> None: - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - schema_defaults = result["data_schema"]({}) - assert CONF_TRACK_NEW not in schema_defaults - assert CONF_SCAN_INTERVAL not in schema_defaults - - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: hosts, - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == f"Nmap Tracker {hosts}" - assert result2["data"] == {} - assert result2["options"] == { - CONF_HOSTS: hosts, - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_range(hass: HomeAssistant) -> None: - """Test we get the form and can take an ip range.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "192.168.0.5-12", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "Nmap Tracker 192.168.0.5-12" - assert result2["data"] == {} - assert result2["options"] == { - CONF_HOSTS: "192.168.0.5-12", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_hosts(hass: HomeAssistant) -> None: - """Test invalid hosts passed in.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "not an ip block", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "form" - assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} - - -async def test_form_already_configured(hass: HomeAssistant) -> None: - """Test duplicate host list.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" - - -async def test_form_invalid_excludes(hass: HomeAssistant) -> None: - """Test invalid excludes passed in.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "3.3.3.3", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "not an exclude", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "form" - assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} - - -async def test_options_flow(hass: HomeAssistant) -> None: - """Test we can edit options.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_HOSTS: "192.168.1.0/24", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - config_entry.add_to_hass(hass) - hass.state = CoreState.stopped - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - assert result["data_schema"]({}) == { - CONF_EXCLUDE: "4.4.4.4", - CONF_HOME_INTERVAL: 3, - CONF_HOSTS: "192.168.1.0/24", - CONF_SCAN_INTERVAL: 120, - CONF_OPTIONS: "-F --host-timeout 5s", - CONF_TRACK_NEW: True, - } - - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24", - CONF_HOME_INTERVAL: 5, - CONF_OPTIONS: "-sn", - CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", - CONF_SCAN_INTERVAL: 10, - CONF_TRACK_NEW: False, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == { - CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", - CONF_HOME_INTERVAL: 5, - CONF_OPTIONS: "-sn", - CONF_EXCLUDE: "4.4.4.4,5.5.5.5", - CONF_SCAN_INTERVAL: 10, - CONF_TRACK_NEW: False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import(hass: HomeAssistant) -> None: - """Test we can import from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOSTS: "1.2.3.4/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", - CONF_SCAN_INTERVAL: 2000, - CONF_TRACK_NEW: False, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "Nmap Tracker 1.2.3.4/20" - assert result["data"] == {} - assert result["options"] == { - CONF_HOSTS: "1.2.3.4/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4,6.4.3.2", - CONF_SCAN_INTERVAL: 2000, - CONF_TRACK_NEW: False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_aborts_if_matching(hass: HomeAssistant) -> None: - """Test we can import from yaml.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - config_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" From f5c3444072e0dc40764d7b5dfb96cf587bde6517 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 6 Jul 2021 19:34:56 +0300 Subject: [PATCH 129/818] Fix Sensibo timeout exceptions (#52513) --- homeassistant/components/sensibo/climate.py | 105 ++++++++++---------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 10ceaa39a38..d34ea040cdc 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -40,7 +40,7 @@ from .const import DOMAIN as SENSIBO_DOMAIN _LOGGER = logging.getLogger(__name__) ALL = ["all"] -TIMEOUT = 10 +TIMEOUT = 8 SERVICE_ASSUME_STATE = "assume_state" @@ -91,17 +91,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) devices = [] try: - for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): - if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]: - devices.append( - SensiboClimate(client, dev, hass.config.units.temperature_unit) - ) + with async_timeout.timeout(TIMEOUT): + for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): + if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]: + devices.append( + SensiboClimate(client, dev, hass.config.units.temperature_unit) + ) except ( aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError, pysensibo.SensiboError, ) as err: - _LOGGER.exception("Failed to connect to Sensibo servers") + _LOGGER.error("Failed to get devices from Sensibo servers") raise PlatformNotReady from err if not devices: @@ -150,6 +151,7 @@ class SensiboClimate(ClimateEntity): self._units = units self._available = False self._do_update(data) + self._failed_update = False @property def supported_features(self): @@ -316,59 +318,35 @@ class SensiboClimate(ClimateEntity): else: return - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "targetTemperature", temperature, self._ac_states - ) + await self._async_set_ac_state_property("targetTemperature", temperature) async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "fanLevel", fan_mode, self._ac_states - ) + await self._async_set_ac_state_property("fanLevel", fan_mode) async def async_set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", False, self._ac_states - ) + await self._async_set_ac_state_property("on", False) return # Turn on if not currently on. if not self._ac_states["on"]: - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", True, self._ac_states - ) + await self._async_set_ac_state_property("on", True) - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "mode", HA_TO_SENSIBO[hvac_mode], self._ac_states - ) + await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "swing", swing_mode, self._ac_states - ) + await self._async_set_ac_state_property("swing", swing_mode) async def async_turn_on(self): """Turn Sensibo unit on.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", True, self._ac_states - ) + await self._async_set_ac_state_property("on", True) async def async_turn_off(self): """Turn Sensibo unit on.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", False, self._ac_states - ) + await self._async_set_ac_state_property("on", False) async def async_assume_state(self, state): """Set external state.""" @@ -377,14 +355,7 @@ class SensiboClimate(ClimateEntity): ) if change_needed: - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, - "on", - state != HVAC_MODE_OFF, # value - self._ac_states, - True, # assumed_state - ) + await self._async_set_ac_state_property("on", state != HVAC_MODE_OFF, True) if state in [STATE_ON, HVAC_MODE_OFF]: self._external_state = None @@ -396,7 +367,41 @@ class SensiboClimate(ClimateEntity): try: with async_timeout.timeout(TIMEOUT): data = await self._client.async_get_device(self._id, _FETCH_FIELDS) - self._do_update(data) - except (aiohttp.client_exceptions.ClientError, pysensibo.SensiboError): - _LOGGER.warning("Failed to connect to Sensibo servers") + except ( + aiohttp.client_exceptions.ClientError, + asyncio.TimeoutError, + pysensibo.SensiboError, + ): + if self._failed_update: + _LOGGER.warning( + "Failed to update data for device '%s' from Sensibo servers", + self.name, + ) + self._available = False + self.async_write_ha_state() + return + + _LOGGER.debug("First failed update data for device '%s'", self.name) + self._failed_update = True + return + + self._failed_update = False + self._do_update(data) + + async def _async_set_ac_state_property(self, name, value, assumed_state=False): + """Set AC state.""" + try: + with async_timeout.timeout(TIMEOUT): + await self._client.async_set_ac_state_property( + self._id, name, value, self._ac_states, assumed_state + ) + except ( + aiohttp.client_exceptions.ClientError, + asyncio.TimeoutError, + pysensibo.SensiboError, + ) as err: self._available = False + self.async_write_ha_state() + raise Exception( + f"Failed to set AC state for device {self.name} to Sensibo servers" + ) from err From 5c07fb51a2ff2caf89850ec96ade59252e9d580e Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 6 Jul 2021 18:48:48 +0200 Subject: [PATCH 130/818] Update Somfy to reduce calls to /site entrypoint (#51572) Co-authored-by: Franck Nijhof --- homeassistant/components/somfy/__init__.py | 95 +------------------ homeassistant/components/somfy/climate.py | 13 ++- homeassistant/components/somfy/const.py | 1 - homeassistant/components/somfy/coordinator.py | 71 ++++++++++++++ homeassistant/components/somfy/cover.py | 13 ++- homeassistant/components/somfy/entity.py | 73 ++++++++++++++ homeassistant/components/somfy/manifest.json | 2 +- homeassistant/components/somfy/sensor.py | 13 ++- homeassistant/components/somfy/switch.py | 13 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/somfy/test_config_flow.py | 11 +-- 12 files changed, 180 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/somfy/coordinator.py create mode 100644 homeassistant/components/somfy/entity.py diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index cc7499c3492..71d7f7f790c 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -1,5 +1,4 @@ """Support for Somfy hubs.""" -from abc import abstractmethod from datetime import timedelta import logging @@ -8,20 +7,16 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) from . import api, config_flow -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .coordinator import SomfyDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -84,25 +79,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) data = hass.data[DOMAIN] - data[API] = api.ConfigEntrySomfyApi(hass, entry, implementation) - - async def _update_all_devices(): - """Update all the devices.""" - devices = await hass.async_add_executor_job(data[API].get_devices) - previous_devices = data[COORDINATOR].data - # Sometimes Somfy returns an empty list. - if not devices and previous_devices: - _LOGGER.debug( - "No devices returned. Assuming the previous ones are still valid" - ) - return previous_devices - return {dev.id: dev for dev in devices} - - coordinator = DataUpdateCoordinator( + coordinator = SomfyDataUpdateCoordinator( hass, _LOGGER, name="somfy device update", - update_method=_update_all_devices, + client=api.ConfigEntrySomfyApi(hass, entry, implementation), update_interval=SCAN_INTERVAL, ) data[COORDINATOR] = coordinator @@ -140,70 +121,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - hass.data[DOMAIN].pop(API, None) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -class SomfyEntity(CoordinatorEntity, Entity): - """Representation of a generic Somfy device.""" - - def __init__(self, coordinator, device_id, somfy_api): - """Initialize the Somfy device.""" - super().__init__(coordinator) - self._id = device_id - self.api = somfy_api - - @property - def device(self): - """Return data for the device id.""" - return self.coordinator.data[self._id] - - @property - def unique_id(self) -> str: - """Return the unique id base on the id returned by Somfy.""" - return self._id - - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.name - - @property - def device_info(self): - """Return device specific attributes. - - Implemented by platform classes. - """ - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "model": self.device.type, - "via_device": (DOMAIN, self.device.parent_id), - # For the moment, Somfy only returns their own device. - "manufacturer": "Somfy", - } - - def has_capability(self, capability: str) -> bool: - """Test if device has a capability.""" - capabilities = self.device.capabilities - return bool([c for c in capabilities if c.name == capability]) - - def has_state(self, state: str) -> bool: - """Test if device has a state.""" - states = self.device.states - return bool([c for c in states if c.name == state]) - - @property - def assumed_state(self) -> bool: - """Return if the device has an assumed state.""" - return not bool(self.device.states) - - @callback - def _handle_coordinator_update(self): - """Process an update from the coordinator.""" - self._create_device() - super()._handle_coordinator_update() - - @abstractmethod - def _create_device(self): - """Update the device with the latest data.""" diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index 66602aea3e6..0963321100c 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -23,8 +23,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity SUPPORTED_CATEGORIES = {Category.HVAC.value} @@ -49,10 +49,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy climate platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] climates = [ - SomfyClimate(coordinator, device_id, api) + SomfyClimate(coordinator, device_id) for device_id, device in coordinator.data.items() if SUPPORTED_CATEGORIES & set(device.categories) ] @@ -63,15 +62,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyClimate(SomfyEntity, ClimateEntity): """Representation of a Somfy thermostat device.""" - def __init__(self, coordinator, device_id, api): + def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self._climate = None self._create_device() def _create_device(self): """Update the device with the latest data.""" - self._climate = Thermostat(self.device, self.api) + self._climate = Thermostat(self.device, self.coordinator.client) @property def supported_features(self) -> int: diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py index 128d6eb76bb..6c7c23e3ab3 100644 --- a/homeassistant/components/somfy/const.py +++ b/homeassistant/components/somfy/const.py @@ -2,4 +2,3 @@ DOMAIN = "somfy" COORDINATOR = "coordinator" -API = "api" diff --git a/homeassistant/components/somfy/coordinator.py b/homeassistant/components/somfy/coordinator.py new file mode 100644 index 00000000000..c9633c4fa4d --- /dev/null +++ b/homeassistant/components/somfy/coordinator.py @@ -0,0 +1,71 @@ +"""Helpers to help coordinate updated.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pymfy.api.error import QuotaViolationException, SetupNotFoundException +from pymfy.api.model import Device +from pymfy.api.somfy_api import SomfyApi + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +class SomfyDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Somfy data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + client: SomfyApi, + update_interval: timedelta | None = None, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + ) + self.data = {} + self.client = client + self.site_device = {} + self.last_site_index = -1 + + async def _async_update_data(self) -> dict[str, Device]: + """Fetch Somfy data. + + Somfy only allow one call per minute to /site. There is one exception: 2 calls are allowed after site retrieval. + """ + if not self.site_device: + sites = await self.hass.async_add_executor_job(self.client.get_sites) + if not sites: + return {} + self.site_device = {site.id: [] for site in sites} + + site_id = self._site_id + try: + devices = await self.hass.async_add_executor_job( + self.client.get_devices, site_id + ) + self.site_device[site_id] = devices + except SetupNotFoundException: + del self.site_device[site_id] + return await self._async_update_data() + except QuotaViolationException: + self.logger.warning("Quota violation") + + return {dev.id: dev for devices in self.site_device.values() for dev in devices} + + @property + def _site_id(self): + """Return the next site id to retrieve. + + This tweak is required as Somfy does not allow to call the /site entrypoint more than once per minute. + """ + self.last_site_index = (self.last_site_index + 1) % len(self.site_device) + return list(self.site_device.keys())[self.last_site_index] diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index d227bc31227..8ed06b3bcd7 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -21,8 +21,8 @@ from homeassistant.components.cover import ( from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN from homeassistant.helpers.restore_state import RestoreEntity -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity BLIND_DEVICE_CATEGORIES = {Category.INTERIOR_BLIND.value, Category.EXTERIOR_BLIND.value} SHUTTER_DEVICE_CATEGORIES = {Category.EXTERIOR_BLIND.value} @@ -37,10 +37,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy cover platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] covers = [ - SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC]) + SomfyCover(coordinator, device_id, domain_data[CONF_OPTIMISTIC]) for device_id, device in coordinator.data.items() if SUPPORTED_CATEGORIES & set(device.categories) ] @@ -51,9 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """Representation of a Somfy cover device.""" - def __init__(self, coordinator, device_id, api, optimistic): + def __init__(self, coordinator, device_id, optimistic): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self.categories = set(self.device.categories) self.optimistic = optimistic self._closed = None @@ -64,7 +63,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): def _create_device(self) -> Blind: """Update the device with the latest data.""" - self._cover = Blind(self.device, self.api) + self._cover = Blind(self.device, self.coordinator.client) @property def supported_features(self) -> int: diff --git a/homeassistant/components/somfy/entity.py b/homeassistant/components/somfy/entity.py new file mode 100644 index 00000000000..88ff86e8849 --- /dev/null +++ b/homeassistant/components/somfy/entity.py @@ -0,0 +1,73 @@ +"""Entity representing a Somfy device.""" + +from abc import abstractmethod + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +class SomfyEntity(CoordinatorEntity, Entity): + """Representation of a generic Somfy device.""" + + def __init__(self, coordinator, device_id): + """Initialize the Somfy device.""" + super().__init__(coordinator) + self._id = device_id + + @property + def device(self): + """Return data for the device id.""" + return self.coordinator.data[self._id] + + @property + def unique_id(self) -> str: + """Return the unique id base on the id returned by Somfy.""" + return self._id + + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device.name + + @property + def device_info(self): + """Return device specific attributes. + + Implemented by platform classes. + """ + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "model": self.device.type, + "via_device": (DOMAIN, self.device.parent_id), + # For the moment, Somfy only returns their own device. + "manufacturer": "Somfy", + } + + def has_capability(self, capability: str) -> bool: + """Test if device has a capability.""" + capabilities = self.device.capabilities + return bool([c for c in capabilities if c.name == capability]) + + def has_state(self, state: str) -> bool: + """Test if device has a state.""" + states = self.device.states + return bool([c for c in states if c.name == state]) + + @property + def assumed_state(self) -> bool: + """Return if the device has an assumed state.""" + return not bool(self.device.states) + + @callback + def _handle_coordinator_update(self): + """Process an update from the coordinator.""" + self._create_device() + super()._handle_coordinator_update() + + @abstractmethod + def _create_device(self): + """Update the device with the latest data.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 8dad4abd6cc..1adbab49fb2 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.9.3"], + "requirements": ["pymfy==0.11.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 312c425cf87..1817ba3fd8c 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -6,8 +6,8 @@ from pymfy.api.devices.thermostat import Thermostat from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity SUPPORTED_CATEGORIES = {Category.HVAC.value} @@ -16,10 +16,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy sensor platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] sensors = [ - SomfyThermostatBatterySensor(coordinator, device_id, api) + SomfyThermostatBatterySensor(coordinator, device_id) for device_id, device in coordinator.data.items() if SUPPORTED_CATEGORIES & set(device.categories) ] @@ -33,15 +32,15 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_unit_of_measurement = PERCENTAGE - def __init__(self, coordinator, device_id, api): + def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self._climate = None self._create_device() def _create_device(self): """Update the device with the latest data.""" - self._climate = Thermostat(self.device, self.api) + self._climate = Thermostat(self.device, self.coordinator.client) @property def state(self) -> int: diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index 66eef99d6b5..bd0b1dce5d5 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -4,18 +4,17 @@ from pymfy.api.devices.category import Category from homeassistant.components.switch import SwitchEntity -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy switch platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] switches = [ - SomfyCameraShutter(coordinator, device_id, api) + SomfyCameraShutter(coordinator, device_id) for device_id, device in coordinator.data.items() if Category.CAMERA.value in device.categories ] @@ -26,14 +25,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyCameraShutter(SomfyEntity, SwitchEntity): """Representation of a Somfy Camera Shutter device.""" - def __init__(self, coordinator, device_id, api): + def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self._create_device() def _create_device(self): """Update the device with the latest data.""" - self.shutter = CameraProtect(self.device, self.api) + self.shutter = CameraProtect(self.device, self.coordinator.client) def turn_on(self, **kwargs) -> None: """Turn the entity on.""" diff --git a/requirements_all.txt b/requirements_all.txt index 80b97205115..65858084e5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1582,7 +1582,7 @@ pymelcloud==2.5.3 pymeteoclimatic==0.0.6 # homeassistant.components.somfy -pymfy==0.9.3 +pymfy==0.11.0 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 306c158d627..fae02a7c4f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -896,7 +896,7 @@ pymelcloud==2.5.3 pymeteoclimatic==0.0.6 # homeassistant.components.somfy -pymfy==0.9.3 +pymfy==0.11.0 # homeassistant.components.mochad pymochad==0.2.0 diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index b7d78883706..6a1c32e4138 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -82,7 +82,9 @@ async def test_full_flow( }, ) - with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"): + with patch( + "homeassistant.components.somfy.async_setup_entry", return_value=True + ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["data"]["auth_implementation"] == DOMAIN @@ -95,12 +97,7 @@ async def test_full_flow( "expires_in": 60, } - assert DOMAIN in hass.config.components - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state is config_entries.ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert len(mock_setup_entry.mock_calls) == 1 async def test_abort_if_authorization_timeout(hass, current_request_with_host): From 2f1af9a2540815bc84ff733942566c0f4d3f4e38 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 6 Jul 2021 13:19:41 -0500 Subject: [PATCH 131/818] Remove unnecessary async_setup method for Guardian (#52597) --- homeassistant/components/guardian/__init__.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 0b2d7c634c5..89ff7768c20 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -32,25 +32,20 @@ from .const import ( ) from .util import GuardianDataUpdateCoordinator -DATA_LAST_SENSOR_PAIR_DUMP = "last_sensor_pair_dump" - PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the Elexa Guardian component.""" - hass.data[DOMAIN] = { - DATA_CLIENT: {}, - DATA_COORDINATOR: {}, - DATA_LAST_SENSOR_PAIR_DUMP: {}, - DATA_PAIRED_SENSOR_MANAGER: {}, - DATA_UNSUB_DISPATCHER_CONNECT: {}, - } - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" + hass.data.setdefault( + DOMAIN, + { + DATA_CLIENT: {}, + DATA_COORDINATOR: {}, + DATA_PAIRED_SENSOR_MANAGER: {}, + DATA_UNSUB_DISPATCHER_CONNECT: {}, + }, + ) client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client( entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT] ) @@ -116,7 +111,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - hass.data[DOMAIN][DATA_LAST_SENSOR_PAIR_DUMP].pop(entry.entry_id) for unsub in hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id]: unsub() hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT].pop(entry.entry_id) From 9fb05736e4a506d5f220de87d7e82f3fe88d982b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 6 Jul 2021 13:19:58 -0500 Subject: [PATCH 132/818] Add type annotations to Ambient PWS (#52596) --- .../components/ambient_station/__init__.py | 48 +++++++++++-------- .../ambient_station/binary_sensor.py | 12 +++-- .../components/ambient_station/config_flow.py | 9 ++-- .../components/ambient_station/sensor.py | 29 ++++++----- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index f276597c79a..d2f09e47f7b 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station Service.""" +from __future__ import annotations from aioambient import Client from aioambient.errors import WebsocketError @@ -8,6 +9,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR, ) from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LOCATION, ATTR_NAME, @@ -30,7 +32,7 @@ from homeassistant.const import ( SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -292,7 +294,7 @@ SENSOR_TYPES = { CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Ambient PWS as config entry.""" hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) @@ -330,7 +332,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload an Ambient PWS config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) @@ -338,7 +340,7 @@ async def async_unload_entry(hass, config_entry): return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) -async def async_migrate_entry(hass, config_entry): +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" version = config_entry.version @@ -362,19 +364,21 @@ async def async_migrate_entry(hass, config_entry): class AmbientStation: """Define a class to handle the Ambient websocket.""" - def __init__(self, hass, config_entry, client): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, client: Client + ) -> None: """Initialize.""" self._config_entry = config_entry self._entry_setup_complete = False self._hass = hass self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client - self.stations = {} + self.stations: dict[str, dict] = {} - async def _attempt_connect(self): + async def _attempt_connect(self) -> None: """Attempt to connect to the socket (retrying later on fail).""" - async def connect(timestamp=None): + async def connect(timestamp: int | None = None): """Connect.""" await self.client.websocket.connect() @@ -385,14 +389,14 @@ class AmbientStation: self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) async_call_later(self._hass, self._ws_reconnect_delay, connect) - async def ws_connect(self): + async def ws_connect(self) -> None: """Register handlers and connect to the websocket.""" - def on_connect(): + def on_connect() -> None: """Define a handler to fire when the websocket is connected.""" LOGGER.info("Connected to websocket") - def on_data(data): + def on_data(data: dict) -> None: """Define a handler to fire when the data is received.""" mac_address = data["macAddress"] if data != self.stations[mac_address][ATTR_LAST_DATA]: @@ -402,11 +406,11 @@ class AmbientStation: self._hass, f"ambient_station_data_update_{mac_address}" ) - def on_disconnect(): + def on_disconnect() -> None: """Define a handler to fire when the websocket is disconnected.""" LOGGER.info("Disconnected from websocket") - def on_subscribed(data): + def on_subscribed(data: dict) -> None: """Define a handler to fire when the subscription is set.""" for station in data["devices"]: if station["macAddress"] in self.stations: @@ -447,7 +451,7 @@ class AmbientStation: await self._attempt_connect() - async def ws_disconnect(self): + async def ws_disconnect(self) -> None: """Disconnect from the websocket.""" await self.client.websocket.disconnect() @@ -456,8 +460,14 @@ class AmbientWeatherEntity(Entity): """Define a base Ambient PWS entity.""" def __init__( - self, ambient, mac_address, station_name, sensor_type, sensor_name, device_class - ): + self, + ambient: AmbientStation, + mac_address: str, + station_name: str, + sensor_type: str, + sensor_name: str, + device_class: str | None, + ) -> None: """Initialize the sensor.""" self._ambient = ambient self._attr_device_class = device_class @@ -472,11 +482,11 @@ class AmbientWeatherEntity(Entity): self._mac_address = mac_address self._sensor_type = sensor_type - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" if self._sensor_type == TYPE_SOLARRADIATION_LX: self._attr_available = ( @@ -505,6 +515,6 @@ class AmbientWeatherEntity(Entity): self.update_from_latest_data() @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the entity from the latest data.""" raise NotImplementedError diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 872b2f4b9fd..093a582791e 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -1,10 +1,14 @@ """Support for Ambient Weather Station binary sensors.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( SENSOR_TYPES, @@ -27,7 +31,9 @@ from . import ( from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Ambient PWS binary sensors based on a config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] @@ -54,7 +60,7 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): """Define an Ambient binary sensor.""" @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Fetch new state data for the entity.""" state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( self._sensor_type diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 5b40300498b..d93d502ac92 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,10 +1,13 @@ """Config flow to configure the Ambient PWS component.""" +from __future__ import annotations + from aioambient import Client from aioambient.errors import AmbientError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_APP_KEY, DOMAIN @@ -15,13 +18,13 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" self.data_schema = vol.Schema( {vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str} ) - async def _show_form(self, errors=None): + async def _show_form(self, errors: dict | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -29,7 +32,7 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return await self._show_form() diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 69d41c035d0..a606b401bc0 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,18 +1,25 @@ """Support for Ambient Weather Station sensors.""" +from __future__ import annotations + from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( SENSOR_TYPES, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX, + AmbientStation, AmbientWeatherEntity, ) from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Ambient PWS sensors based on a config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] @@ -41,14 +48,14 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): def __init__( self, - ambient, - mac_address, - station_name, - sensor_type, - sensor_name, - device_class, - unit, - ): + ambient: AmbientStation, + mac_address: str, + station_name: str, + sensor_type: str, + sensor_name: str, + device_class: str | None, + unit: str | None, + ) -> None: """Initialize the sensor.""" super().__init__( ambient, mac_address, station_name, sensor_type, sensor_name, device_class @@ -57,7 +64,7 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): self._attr_unit_of_measurement = unit @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" if self._sensor_type == TYPE_SOLARRADIATION_LX: # If the user requests the solarradiation_lx sensor, use the From 16f1647ad0aeb0c4295d741d6b9dbb3f60e49909 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 6 Jul 2021 23:39:25 +0200 Subject: [PATCH 133/818] Python 3.9.6 / Base image 2021.07.0 (#52605) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index c3a3eec0bee..006d182c99d 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.2", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.2", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.2", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.2", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.2" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.07.0", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.07.0", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.07.0", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.07.0", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.07.0" }, "labels": { "io.hass.type": "core", From 0c5ce9cac232cef3738b5875df14ba456367eac7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 7 Jul 2021 00:11:57 +0000 Subject: [PATCH 134/818] [ci skip] Translation update --- .../components/agent_dvr/translations/he.json | 2 +- .../components/airvisual/translations/he.json | 2 +- .../alarm_control_panel/translations/he.json | 7 +++--- .../alarmdecoder/translations/he.json | 2 +- .../ambiclimate/translations/he.json | 11 ++++++++++ .../automation/translations/he.json | 2 +- .../binary_sensor/translations/he.json | 22 +++++++++---------- .../components/calendar/translations/he.json | 2 +- .../components/camera/translations/he.json | 2 +- .../components/cast/translations/de.json | 2 +- .../components/cast/translations/he.json | 2 +- .../cert_expiry/translations/he.json | 3 ++- .../components/climate/translations/he.json | 19 ++++++++++++++-- .../components/fan/translations/he.json | 2 +- .../flick_electric/translations/he.json | 6 ++--- .../forecast_solar/translations/ru.json | 4 ++-- .../forked_daapd/translations/he.json | 1 + .../components/gree/translations/de.json | 2 +- .../components/group/translations/he.json | 2 +- .../homematicip_cloud/translations/he.json | 2 +- .../components/hue/translations/he.json | 8 +++---- .../components/ios/translations/de.json | 2 +- .../components/juicenet/translations/he.json | 4 ++-- .../components/konnected/translations/he.json | 5 +++++ .../components/kraken/translations/de.json | 2 +- .../components/kulersky/translations/de.json | 2 +- .../components/local_ip/translations/de.json | 2 +- .../components/locative/translations/de.json | 2 +- .../modern_forms/translations/de.json | 2 +- .../components/neato/translations/de.json | 2 +- .../components/nest/translations/he.json | 2 +- .../components/person/translations/he.json | 2 +- .../components/plaato/translations/de.json | 2 +- .../components/plant/translations/he.json | 2 +- .../components/point/translations/de.json | 2 +- .../components/poolsense/translations/de.json | 2 +- .../components/profiler/translations/de.json | 2 +- .../components/proximity/translations/he.json | 2 +- .../components/remote/translations/he.json | 4 ++-- .../components/rpi_power/translations/de.json | 2 +- .../components/script/translations/he.json | 2 +- .../components/sensor/translations/he.json | 2 +- .../simplisafe/translations/ca.json | 2 +- .../simplisafe/translations/et.json | 2 +- .../simplisafe/translations/he.json | 3 ++- .../simplisafe/translations/ru.json | 2 +- .../smartthings/translations/he.json | 2 +- .../speedtestdotnet/translations/de.json | 2 +- .../speedtestdotnet/translations/he.json | 3 ++- .../components/switch/translations/he.json | 2 +- .../components/tibber/translations/he.json | 2 +- .../components/timer/translations/he.json | 2 +- .../components/tuya/translations/he.json | 2 +- .../components/twilio/translations/de.json | 2 +- .../components/upb/translations/he.json | 2 +- .../components/updater/translations/he.json | 2 +- .../yamaha_musiccast/translations/de.json | 2 +- .../components/zerproc/translations/de.json | 2 +- .../components/zerproc/translations/he.json | 2 +- 59 files changed, 113 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/agent_dvr/translations/he.json b/homeassistant/components/agent_dvr/translations/he.json index e50d45e5608..d37b99a2f45 100644 --- a/homeassistant/components/agent_dvr/translations/he.json +++ b/homeassistant/components/agent_dvr/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index 5dfc5cbdd73..3a9e5a1cb18 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "general_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", - "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "location_not_found": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0" }, "step": { diff --git a/homeassistant/components/alarm_control_panel/translations/he.json b/homeassistant/components/alarm_control_panel/translations/he.json index 544b23f5629..836194caa80 100644 --- a/homeassistant/components/alarm_control_panel/translations/he.json +++ b/homeassistant/components/alarm_control_panel/translations/he.json @@ -2,15 +2,16 @@ "state": { "_": { "armed": "\u05d3\u05e8\u05d5\u05da", - "armed_away": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "armed_away": "\u05d3\u05e8\u05d5\u05da - \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", "armed_custom_bypass": "\u05de\u05e2\u05e7\u05e3 \u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea \u05d3\u05e8\u05d5\u05da", "armed_home": "\u05d4\u05d1\u05d9\u05ea \u05d3\u05e8\u05d5\u05da", - "armed_night": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4", + "armed_night": "\u05d3\u05e8\u05d5\u05da - \u05dc\u05d9\u05dc\u05d4", + "armed_vacation": "\u05d3\u05e8\u05d5\u05da - \u05d7\u05d5\u05e4\u05e9\u05d4", "arming": "\u05de\u05e4\u05e2\u05d9\u05dc", "disarmed": "\u05de\u05e0\u05d5\u05d8\u05e8\u05dc", "disarming": "\u05de\u05e0\u05d8\u05e8\u05dc", "pending": "\u05de\u05de\u05ea\u05d9\u05df", - "triggered": "\u05d4\u05d5\u05e4\u05e2\u05dc" + "triggered": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05dc\u05d5\u05d7 \u05d1\u05e7\u05e8\u05d4 \u05e9\u05dc \u05d0\u05d6\u05e2\u05e7\u05d4" diff --git a/homeassistant/components/alarmdecoder/translations/he.json b/homeassistant/components/alarmdecoder/translations/he.json index e130a1997b2..db754768f48 100644 --- a/homeassistant/components/alarmdecoder/translations/he.json +++ b/homeassistant/components/alarmdecoder/translations/he.json @@ -28,7 +28,7 @@ "step": { "init": { "data": { - "edit_select": "\u05e2\u05e8\u05d5\u05da" + "edit_select": "\u05e2\u05e8\u05d9\u05db\u05d4" } }, "zone_details": { diff --git a/homeassistant/components/ambiclimate/translations/he.json b/homeassistant/components/ambiclimate/translations/he.json index 7b7ec9c8c30..dc9f86871a7 100644 --- a/homeassistant/components/ambiclimate/translations/he.json +++ b/homeassistant/components/ambiclimate/translations/he.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "access_token": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4.", "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." }, "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "error": { + "follow_link": "\u05d9\u05e9 \u05dc\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d5\u05dc\u05d0\u05de\u05ea \u05d0\u05d5\u05ea\u05d5 \u05dc\u05e4\u05e0\u05d9 \u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7", + "no_token": "\u05dc\u05d0 \u05de\u05d0\u05d5\u05de\u05ea \u05e2\u05dd Ambiclimate" + }, + "step": { + "auth": { + "description": "\u05e0\u05d0 \u05dc\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8\u05d9 [\u05e7\u05d9\u05e9\u05d5\u05e8]({authorization_url}) **\u05d5\u05dc\u05d0\u05e4\u05e9\u05e8** \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d7\u05e9\u05d1\u05d5\u05df \u05d4-Ambiclimate \u05e9\u05dc\u05da, \u05d5\u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05dc\u05d7\u05d6\u05d5\u05e8 \u05d5\u05dc\u05dc\u05d7\u05d5\u05e5 \u05e2\u05dc **\u05e9\u05dc\u05d7** \u05dc\u05de\u05d8\u05d4.\n(\u05e0\u05d0 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05d4\u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d4\u05ea\u05e7\u05e9\u05e8\u05d5\u05ea \u05d7\u05d5\u05d6\u05e8\u05ea \u05d4\u05d5\u05d0 {cb_url})", + "title": "\u05d0\u05de\u05ea \u05d0\u05ea Ambiclimate" + } } } } \ No newline at end of file diff --git a/homeassistant/components/automation/translations/he.json b/homeassistant/components/automation/translations/he.json index 6e4decfce9a..0b94cedbebd 100644 --- a/homeassistant/components/automation/translations/he.json +++ b/homeassistant/components/automation/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05d0\u05d5\u05d8\u05d5\u05de\u05e6\u05d9\u05d4" diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index b7375f3175b..31a75c3acdf 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -16,7 +16,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" }, "battery": { "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", @@ -27,8 +27,8 @@ "on": "\u05e0\u05d8\u05e2\u05df" }, "cold": { - "off": "\u05e8\u05d2\u05d9\u05dc", - "on": "\u05e7\u05b7\u05e8" + "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", + "on": "\u05e7\u05e8" }, "connectivity": { "off": "\u05de\u05e0\u05d5\u05ea\u05e7", @@ -36,7 +36,7 @@ }, "door": { "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", - "on": "\u05e4\u05ea\u05d5\u05d7\u05d4" + "on": "\u05e4\u05ea\u05d5\u05d7" }, "garage_door": { "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", @@ -56,7 +56,7 @@ }, "lock": { "off": "\u05e0\u05e2\u05d5\u05dc", - "on": "\u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc" + "on": "\u05e4\u05ea\u05d5\u05d7" }, "moisture": { "off": "\u05d9\u05d1\u05e9", @@ -64,7 +64,7 @@ }, "motion": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d6\u05d5\u05d4\u05d4" + "on": "\u05d0\u05d5\u05ea\u05e8" }, "moving": { "off": "\u05dc\u05d0 \u05d6\u05d6", @@ -72,18 +72,18 @@ }, "occupancy": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d6\u05d5\u05d4\u05d4" + "on": "\u05d0\u05d5\u05ea\u05e8" }, "opening": { - "off": "\u05e1\u05d2\u05d5\u05e8", + "off": "\u05e0\u05e1\u05d2\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "plug": { "off": "\u05de\u05e0\u05d5\u05ea\u05e7" }, "presence": { - "off": "\u05dc\u05d0 \u05e0\u05d5\u05db\u05d7", - "on": "\u05e0\u05d5\u05db\u05d7" + "off": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "on": "\u05d1\u05d1\u05d9\u05ea" }, "problem": { "off": "\u05ea\u05e7\u05d9\u05df", @@ -106,7 +106,7 @@ "on": "\u05d0\u05d5\u05ea\u05e8" }, "window": { - "off": "\u05e1\u05d2\u05d5\u05e8", + "off": "\u05e0\u05e1\u05d2\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" } }, diff --git a/homeassistant/components/calendar/translations/he.json b/homeassistant/components/calendar/translations/he.json index 206528ef6a8..c80aaab5c1e 100644 --- a/homeassistant/components/calendar/translations/he.json +++ b/homeassistant/components/calendar/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05dc\u05d5\u05bc\u05d7\u05b7 \u05e9\u05c1\u05b8\u05e0\u05b8\u05d4" diff --git a/homeassistant/components/camera/translations/he.json b/homeassistant/components/camera/translations/he.json index ca6b207762d..b3e16e70826 100644 --- a/homeassistant/components/camera/translations/he.json +++ b/homeassistant/components/camera/translations/he.json @@ -1,7 +1,7 @@ { "state": { "_": { - "idle": "\u05de\u05d7\u05db\u05d4", + "idle": "\u05de\u05de\u05ea\u05d9\u05df", "recording": "\u05de\u05e7\u05dc\u05d9\u05d8", "streaming": "\u05de\u05d6\u05e8\u05d9\u05dd" } diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 4d029be9603..98bddf0d7d0 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -15,7 +15,7 @@ "title": "Google Cast-Konfiguration" }, "confirm": { - "description": "M\u00f6chtest du Google Cast einrichten?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index e361c092b09..3a9552980e6 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -15,7 +15,7 @@ "title": "\u05ea\u05e6\u05d5\u05e8\u05ea Google Cast" }, "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Google Cast?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" } } }, diff --git a/homeassistant/components/cert_expiry/translations/he.json b/homeassistant/components/cert_expiry/translations/he.json index 9b55d58684e..1e14311ef67 100644 --- a/homeassistant/components/cert_expiry/translations/he.json +++ b/homeassistant/components/cert_expiry/translations/he.json @@ -11,7 +11,8 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/climate/translations/he.json b/homeassistant/components/climate/translations/he.json index fe5380a0528..abf976c2b5b 100644 --- a/homeassistant/components/climate/translations/he.json +++ b/homeassistant/components/climate/translations/he.json @@ -1,8 +1,23 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "\u05e9\u05e0\u05d4 \u05de\u05e6\u05d1 HVAC \u05d1-{entity_name}", + "set_preset_mode": "\u05e9\u05e0\u05d4 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05e7\u05d1\u05d5\u05e2\u05d4 \u05de\u05e8\u05d0\u05e9 \u05d1-{entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u05de\u05d5\u05d2\u05d3\u05e8 \u05dc\u05de\u05e6\u05d1 HVAC \u05e1\u05e4\u05e6\u05d9\u05e4\u05d9", + "is_preset_mode": "{entity_name} \u05de\u05d5\u05d2\u05d3\u05e8 \u05dc\u05de\u05e6\u05d1 \u05e1\u05e4\u05e6\u05d9\u05e4\u05d9 \u05d4\u05de\u05d5\u05d2\u05d3\u05e8 \u05de\u05e8\u05d0\u05e9" + }, + "trigger_type": { + "current_humidity_changed": "\u05d4\u05dc\u05d7\u05d5\u05ea \u05d4\u05e0\u05de\u05d3\u05d3\u05ea {entity_name} \u05d4\u05e9\u05ea\u05e0\u05ea\u05d4", + "current_temperature_changed": "\u05d4\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05d4\u05e0\u05de\u05d3\u05d3\u05ea \u05e9\u05dc {entity_name} \u05d4\u05e9\u05ea\u05e0\u05ea\u05d4", + "hvac_mode_changed": "{entity_name} \u05de\u05de\u05e6\u05d1 HVAC \u05d4\u05e9\u05ea\u05e0\u05d4" + } + }, "state": { "_": { "auto": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", - "cool": "\u05e7\u05e8\u05d5\u05e8", + "cool": "\u05e7\u05d9\u05e8\u05d5\u05e8", "dry": "\u05d9\u05d1\u05e9", "fan_only": "\u05de\u05d0\u05d5\u05d5\u05e8\u05e8 \u05d1\u05dc\u05d1\u05d3", "heat": "\u05d7\u05d9\u05de\u05d5\u05dd", @@ -10,5 +25,5 @@ "off": "\u05db\u05d1\u05d5\u05d9" } }, - "title": "\u05d0\u05b7\u05e7\u05dc\u05b4\u05d9\u05dd" + "title": "\u05d0\u05e7\u05dc\u05d9\u05dd" } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/he.json b/homeassistant/components/fan/translations/he.json index 63139b0fe34..db876480dfc 100644 --- a/homeassistant/components/fan/translations/he.json +++ b/homeassistant/components/fan/translations/he.json @@ -8,7 +8,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05de\u05d0\u05d5\u05d5\u05e8\u05e8" diff --git a/homeassistant/components/flick_electric/translations/he.json b/homeassistant/components/flick_electric/translations/he.json index 658cdb97588..b1e5464047b 100644 --- a/homeassistant/components/flick_electric/translations/he.json +++ b/homeassistant/components/flick_electric/translations/he.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u05d7\u05e9\u05d1\u05d5\u05df \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/forecast_solar/translations/ru.json b/homeassistant/components/forecast_solar/translations/ru.json index 1becfb2f5cb..bd1e4ae70c0 100644 --- a/homeassistant/components/forecast_solar/translations/ru.json +++ b/homeassistant/components/forecast_solar/translations/ru.json @@ -10,7 +10,7 @@ "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u044f\u0445." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Forecast.Solar." } } }, @@ -24,7 +24,7 @@ "declination": "\u0421\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 (0 = \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435)", "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)" }, - "description": "\u042d\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 Forecast.Solar." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Forecast.Solar." } } } diff --git a/homeassistant/components/forked_daapd/translations/he.json b/homeassistant/components/forked_daapd/translations/he.json index 31c52a3faad..39bd36133d5 100644 --- a/homeassistant/components/forked_daapd/translations/he.json +++ b/homeassistant/components/forked_daapd/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { + "unknown_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", "wrong_host_or_port": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8. \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d5\u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4.", "wrong_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4." }, diff --git a/homeassistant/components/gree/translations/de.json b/homeassistant/components/gree/translations/de.json index 86bc8e36730..19cd4b8c70e 100644 --- a/homeassistant/components/gree/translations/de.json +++ b/homeassistant/components/gree/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index be7c2657e9f..f0aa3b0c2d8 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -1,7 +1,7 @@ { "state": { "_": { - "closed": "\u05e1\u05d2\u05d5\u05e8", + "closed": "\u05e0\u05e1\u05d2\u05e8", "home": "\u05d1\u05d1\u05d9\u05ea", "locked": "\u05e0\u05e2\u05d5\u05dc", "not_home": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", diff --git a/homeassistant/components/homematicip_cloud/translations/he.json b/homeassistant/components/homematicip_cloud/translations/he.json index b6a75868d3b..55260a82f9d 100644 --- a/homeassistant/components/homematicip_cloud/translations/he.json +++ b/homeassistant/components/homematicip_cloud/translations/he.json @@ -22,7 +22,7 @@ }, "link": { "description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc \u05d1\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05d5\u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e9\u05dc\u05d9\u05d7\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d7\u05d1\u05e8 \u05d0\u05ea HomematicIP \u05e2\u05ddHome Assistant.\n\n![\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d1\u05de\u05d2\u05e9\u05e8](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "\u05d7\u05d1\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4" + "title": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e7\u05d9\u05e9\u05d5\u05e8" } } } diff --git a/homeassistant/components/hue/translations/he.json b/homeassistant/components/hue/translations/he.json index c014b0a52ae..ece439b376b 100644 --- a/homeassistant/components/hue/translations/he.json +++ b/homeassistant/components/hue/translations/he.json @@ -2,15 +2,15 @@ "config": { "abort": { "all_configured": "\u05db\u05dc \u05d4\u05de\u05d2\u05e9\u05e8\u05d9\u05dd \u05e9\u05dc Philips Hue \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd \u05db\u05d1\u05e8", - "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "discover_timeout": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d2\u05dc\u05d5\u05ea \u05de\u05d2\u05e9\u05e8\u05d9\u05dd", "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 Philips Hue", - "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { - "linking": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4.", + "linking": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", "register_failed": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." }, "step": { diff --git a/homeassistant/components/ios/translations/de.json b/homeassistant/components/ios/translations/de.json index bc427bd2992..d0927f55f00 100644 --- a/homeassistant/components/ios/translations/de.json +++ b/homeassistant/components/ios/translations/de.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/juicenet/translations/he.json b/homeassistant/components/juicenet/translations/he.json index 384ea203a51..c5a985fc64e 100644 --- a/homeassistant/components/juicenet/translations/he.json +++ b/homeassistant/components/juicenet/translations/he.json @@ -4,9 +4,9 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index c31f537e698..9e791916df7 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -23,6 +23,11 @@ }, "options": { "step": { + "options_binary": { + "data": { + "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } + }, "options_io": { "description": "\u05d4\u05ea\u05d2\u05dc\u05d4 {model} \u05d1-{host} . \u05d1\u05d7\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d1\u05e1\u05d9\u05e1 \u05e9\u05dc \u05db\u05dc \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05dc\u05de\u05d8\u05d4 - \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d6\u05d4 \u05e2\u05e9\u05d5\u05d9 \u05dc\u05d0\u05e4\u05e9\u05e8 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd (\u05de\u05d2\u05e2\u05d9\u05dd \u05e4\u05ea\u05d5\u05d7\u05d9\u05dd/\u05e1\u05d2\u05d5\u05e8\u05d9\u05dd), \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d3\u05d9\u05d2\u05d9\u05d8\u05dc\u05d9\u05d9\u05dd (dht \u05d5-ds18b20), \u05d0\u05d5 \u05d9\u05e6\u05d9\u05d0\u05d5\u05ea \u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d7\u05dc\u05e4\u05d4. \u05ea\u05d5\u05db\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05e4\u05d5\u05e8\u05d8\u05d5\u05ea \u05d1\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05d1\u05d0\u05d9\u05dd." } diff --git a/homeassistant/components/kraken/translations/de.json b/homeassistant/components/kraken/translations/de.json index d0a845edfd4..5a6df75c402 100644 --- a/homeassistant/components/kraken/translations/de.json +++ b/homeassistant/components/kraken/translations/de.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json index 86bc8e36730..19cd4b8c70e 100644 --- a/homeassistant/components/kulersky/translations/de.json +++ b/homeassistant/components/kulersky/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/local_ip/translations/de.json b/homeassistant/components/local_ip/translations/de.json index 345874615ec..07aa81c0c3e 100644 --- a/homeassistant/components/local_ip/translations/de.json +++ b/homeassistant/components/local_ip/translations/de.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", "title": "Lokale IP-Adresse" } } diff --git a/homeassistant/components/locative/translations/de.json b/homeassistant/components/locative/translations/de.json index 2df9f889e85..5ca00363476 100644 --- a/homeassistant/components/locative/translations/de.json +++ b/homeassistant/components/locative/translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", "title": "Locative Webhook einrichten" } } diff --git a/homeassistant/components/modern_forms/translations/de.json b/homeassistant/components/modern_forms/translations/de.json index 644f525179e..3a289996ee9 100644 --- a/homeassistant/components/modern_forms/translations/de.json +++ b/homeassistant/components/modern_forms/translations/de.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index dac965a13bf..4b0722a207c 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -15,7 +15,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "title": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "title": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 91274d8b731..dab438c24a8 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -14,7 +14,7 @@ "internal_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05e4\u05e0\u05d9\u05de\u05d9\u05ea \u05d1\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3", "invalid_pin": "\u05e7\u05d5\u05d3 PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3" + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "init": { diff --git a/homeassistant/components/person/translations/he.json b/homeassistant/components/person/translations/he.json index 8bc21b19133..1c36d16f936 100644 --- a/homeassistant/components/person/translations/he.json +++ b/homeassistant/components/person/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "home": "\u05d1\u05d1\u05d9\u05ea", - "not_home": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0" + "not_home": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea" } }, "title": "\u05d0\u05d3\u05dd" diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index 8d13e5d8cb0..6e02096d0b4 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -27,7 +27,7 @@ "device_name": "Benenne dein Ger\u00e4t", "device_type": "Art des Plaato-Ger\u00e4ts" }, - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", "title": "Plaato Ger\u00e4te einrichten" }, "webhook": { diff --git a/homeassistant/components/plant/translations/he.json b/homeassistant/components/plant/translations/he.json index 0263e4da389..a9948ce8765 100644 --- a/homeassistant/components/plant/translations/he.json +++ b/homeassistant/components/plant/translations/he.json @@ -5,5 +5,5 @@ "problem": "\u05d1\u05e2\u05d9\u05d4" } }, - "title": "\u05e6\u05de\u05d7" + "title": "\u05de\u05e0\u05d8\u05e8 \u05e6\u05de\u05d7\u05d9\u05dd" } \ No newline at end of file diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index 41a8eb4344f..c2764e08da9 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -23,7 +23,7 @@ "data": { "flow_impl": "Anbieter" }, - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", "title": "W\u00e4hle die Authentifizierungsmethode" } } diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index dc569c2d9ad..0f82f949ede 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -12,7 +12,7 @@ "email": "E-Mail", "password": "Passwort" }, - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", "title": "" } } diff --git a/homeassistant/components/profiler/translations/de.json b/homeassistant/components/profiler/translations/de.json index 7137cd2ee4e..31df88ebe98 100644 --- a/homeassistant/components/profiler/translations/de.json +++ b/homeassistant/components/profiler/translations/de.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/proximity/translations/he.json b/homeassistant/components/proximity/translations/he.json index 1e840a5f52e..de60856aab1 100644 --- a/homeassistant/components/proximity/translations/he.json +++ b/homeassistant/components/proximity/translations/he.json @@ -1,3 +1,3 @@ { - "title": "\u05e7\u05b4\u05e8\u05d1\u05b8\u05d4" + "title": "\u05e7\u05d9\u05e8\u05d1\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/he.json b/homeassistant/components/remote/translations/he.json index 4b6283c1811..0cf0d53cd88 100644 --- a/homeassistant/components/remote/translations/he.json +++ b/homeassistant/components/remote/translations/he.json @@ -17,8 +17,8 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, - "title": "\u05de\u05b0\u05e8\u05d5\u05bc\u05d7\u05b8\u05e7" + "title": "\u05de\u05e8\u05d5\u05d7\u05e7" } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/de.json b/homeassistant/components/rpi_power/translations/de.json index 1a00c87b985..cc830bce135 100644 --- a/homeassistant/components/rpi_power/translations/de.json +++ b/homeassistant/components/rpi_power/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/script/translations/he.json b/homeassistant/components/script/translations/he.json index 7c2b808c58d..26d8f95b91c 100644 --- a/homeassistant/components/script/translations/he.json +++ b/homeassistant/components/script/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05e1\u05e7\u05e8\u05d9\u05e4\u05d8" diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 2fec6a8d81f..8efc4e37126 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05d7\u05d9\u05d9\u05e9\u05df" diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index e8bb80d1b88..9eb1390466c 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -19,7 +19,7 @@ "data": { "password": "Contrasenya" }, - "description": "L'acc\u00e9s ha caducat o ha estat revocat. Introdueix la teva contrasenya per tornar a vincular el compte.", + "description": "L'acc\u00e9s ha caducat o ha estat revocat. Introdueix la contrasenya per tornar a vincular el compte.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index b517a83498a..7eea7dc100d 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -19,7 +19,7 @@ "data": { "password": "Salas\u00f5na" }, - "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto uuesti sidumiseks sisesta salas\u00f5na.", + "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto taassidumiseks sisesta salas\u00f5na.", "title": "Taastuvasta SimpliSafe'i konto" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index dd3969f269f..6dcf2c0c07b 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -12,7 +12,8 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da." + "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da.", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, "user": { "data": { diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index bcfffc57533..5fc3fce065e 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -19,7 +19,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", + "description": "\u0421\u0440\u043e\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index 56fd6e0b4fb..c098bfafbee 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -20,7 +20,7 @@ }, "user": { "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", - "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9 " + "title": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d4\u05ea\u05e7\u05e9\u05e8\u05d5\u05ea \u05d7\u05d6\u05e8\u05d4" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index f2635c19f03..79a47cbcd2e 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/speedtestdotnet/translations/he.json b/homeassistant/components/speedtestdotnet/translations/he.json index 08506bf3437..ea3dd80aa94 100644 --- a/homeassistant/components/speedtestdotnet/translations/he.json +++ b/homeassistant/components/speedtestdotnet/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "wrong_server_id": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e8\u05ea \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9" }, "step": { "user": { diff --git a/homeassistant/components/switch/translations/he.json b/homeassistant/components/switch/translations/he.json index 23fbb7755f3..0b70a69350b 100644 --- a/homeassistant/components/switch/translations/he.json +++ b/homeassistant/components/switch/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05de\u05ea\u05d2" diff --git a/homeassistant/components/tibber/translations/he.json b/homeassistant/components/tibber/translations/he.json index 780c7990217..3deeeef2e9c 100644 --- a/homeassistant/components/tibber/translations/he.json +++ b/homeassistant/components/tibber/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05d7\u05e9\u05d1\u05d5\u05df Tibber \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/timer/translations/he.json b/homeassistant/components/timer/translations/he.json index 2203ca93e5b..14e21d4bfef 100644 --- a/homeassistant/components/timer/translations/he.json +++ b/homeassistant/components/timer/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "active": "\u05e4\u05e2\u05d9\u05dc", - "idle": "\u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc", + "idle": "\u05de\u05de\u05ea\u05d9\u05df", "paused": "\u05de\u05d5\u05e9\u05d4\u05d4" } } diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 0a05bec6b21..45980842a75 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -14,7 +14,7 @@ "data": { "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05d4 \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da (\u05dc\u05de\u05e9\u05dc, 1 \u05dc\u05d0\u05e8\u05d4\"\u05d1 \u05d0\u05d5 972 \u05dc\u05d9\u05e9\u05e8\u05d0\u05dc)", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "platform": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05d1\u05d4 \u05e8\u05e9\u05d5\u05dd \u05d7\u05e9\u05d1\u05d5\u05e0\u05da", + "platform": "\u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05d1\u05d5 \u05e8\u05e9\u05d5\u05dd \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d8\u05d5\u05d9\u05d4 \u05e9\u05dc\u05da.", diff --git a/homeassistant/components/twilio/translations/de.json b/homeassistant/components/twilio/translations/de.json index d9f071e8ff7..73a3f244f96 100644 --- a/homeassistant/components/twilio/translations/de.json +++ b/homeassistant/components/twilio/translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", "title": "Twilio-Webhook einrichten" } } diff --git a/homeassistant/components/upb/translations/he.json b/homeassistant/components/upb/translations/he.json index e89a02adfa4..7c7490161fe 100644 --- a/homeassistant/components/upb/translations/he.json +++ b/homeassistant/components/upb/translations/he.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/updater/translations/he.json b/homeassistant/components/updater/translations/he.json index de8c2468f90..38072833421 100644 --- a/homeassistant/components/updater/translations/he.json +++ b/homeassistant/components/updater/translations/he.json @@ -1,3 +1,3 @@ { - "title": "\u05d4\u05de\u05e2\u05d3\u05db\u05df" + "title": "\u05de\u05e2\u05d3\u05db\u05df" } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json index 28022aec025..526e480602a 100644 --- a/homeassistant/components/yamaha_musiccast/translations/de.json +++ b/homeassistant/components/yamaha_musiccast/translations/de.json @@ -10,7 +10,7 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, "user": { "data": { diff --git a/homeassistant/components/zerproc/translations/de.json b/homeassistant/components/zerproc/translations/de.json index dfc337fc844..19cd4b8c70e 100644 --- a/homeassistant/components/zerproc/translations/de.json +++ b/homeassistant/components/zerproc/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du die Installation starten?" + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/zerproc/translations/he.json b/homeassistant/components/zerproc/translations/he.json index e228b3719d1..459053197a6 100644 --- a/homeassistant/components/zerproc/translations/he.json +++ b/homeassistant/components/zerproc/translations/he.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" } } } From 88cd7f481df7e14c0db070d74c4787992b5301ae Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 7 Jul 2021 02:30:48 -0400 Subject: [PATCH 135/818] Bump up ZHA dependencies (#52611) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 68feabb18b4..d37abea2310 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.25.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.58", + "zha-quirks==0.0.59", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", "zigpy==0.35.1", diff --git a/requirements_all.txt b/requirements_all.txt index 65858084e5f..bdd7dd3ba6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2426,7 +2426,7 @@ zengge==0.2 zeroconf==0.32.1 # homeassistant.components.zha -zha-quirks==0.0.58 +zha-quirks==0.0.59 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fae02a7c4f5..dd2157ccc61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1326,7 +1326,7 @@ zeep[async]==4.0.0 zeroconf==0.32.1 # homeassistant.components.zha -zha-quirks==0.0.58 +zha-quirks==0.0.59 # homeassistant.components.zha zigpy-cc==0.5.2 From 1ba5c1c9fb1e380549cb655986b5f4d3873d7352 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jul 2021 02:23:24 -0500 Subject: [PATCH 136/818] Fix deadlock at shutdown with python 3.9 (#52613) --- homeassistant/runner.py | 10 ---------- homeassistant/util/executor.py | 2 +- tests/util/test_executor.py | 12 ++++++------ 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 86bebecb7b1..5eae0b1b2da 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -73,16 +73,6 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid loop.set_default_executor = warn_use( # type: ignore loop.set_default_executor, "sets default executor on the event loop" ) - - # Shut down executor when we shut down loop - orig_close = loop.close - - def close() -> None: - executor.logged_shutdown() - orig_close() - - loop.close = close # type: ignore - return loop diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index c25c6b9c13f..9277e396bc4 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -62,7 +62,7 @@ def join_or_interrupt_threads( class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): """A ThreadPoolExecutor instance that will not deadlock on shutdown.""" - def logged_shutdown(self) -> None: + def shutdown(self, *args, **kwargs) -> None: # type: ignore """Shutdown backport from cpython 3.9 with interrupt support added.""" with self._shutdown_lock: # type: ignore[attr-defined] self._shutdown = True diff --git a/tests/util/test_executor.py b/tests/util/test_executor.py index 911145ecc4e..eaa48c75d1a 100644 --- a/tests/util/test_executor.py +++ b/tests/util/test_executor.py @@ -24,7 +24,7 @@ async def test_executor_shutdown_can_interrupt_threads(caplog): for _ in range(100): sleep_futures.append(iexecutor.submit(_loop_sleep_in_executor)) - iexecutor.logged_shutdown() + iexecutor.shutdown() for future in sleep_futures: with pytest.raises((concurrent.futures.CancelledError, SystemExit)): @@ -45,13 +45,13 @@ async def test_executor_shutdown_only_logs_max_attempts(caplog): iexecutor.submit(_loop_sleep_in_executor) with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.3): - iexecutor.logged_shutdown() + iexecutor.shutdown() assert "time.sleep(0.2)" in caplog.text assert ( caplog.text.count("is still running at shutdown") == executor.MAX_LOG_ATTEMPTS ) - iexecutor.logged_shutdown() + iexecutor.shutdown() async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog): @@ -65,7 +65,7 @@ async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog): for _ in range(5): iexecutor.submit(_do_nothing) - iexecutor.logged_shutdown() + iexecutor.shutdown() assert "is still running at shutdown" not in caplog.text @@ -83,9 +83,9 @@ async def test_overall_timeout_reached(caplog): start = time.monotonic() with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.5): - iexecutor.logged_shutdown() + iexecutor.shutdown() finish = time.monotonic() assert finish - start < 1 - iexecutor.logged_shutdown() + iexecutor.shutdown() From c5d806fdbe179799d15a15d7f291bea5aa988e83 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Jul 2021 09:46:59 +0200 Subject: [PATCH 137/818] Fix broadlink creating duplicate unique IDs (#52621) --- homeassistant/components/broadlink/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 1f599d6d108..1576c8b8418 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -146,7 +146,6 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): self._attr_assumed_state = True self._attr_device_class = DEVICE_CLASS_SWITCH self._attr_name = f"{self._device.name} Switch" - self._attr_unique_id = self._device.unique_id @property def is_on(self): @@ -215,6 +214,7 @@ class BroadlinkSP1Switch(BroadlinkSwitch): def __init__(self, device): """Initialize the switch.""" super().__init__(device, 1, 0) + self._attr_unique_id = self._device.unique_id async def _async_send_packet(self, packet): """Send a packet to the device.""" From 45fbc18eb02ae3c444d1412945a3d734289b390a Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Jul 2021 01:34:14 -0700 Subject: [PATCH 138/818] Fix mysensors rgb light (#52604) * remove assert self._white as not all RGB will have a white channel * suggested change * Update homeassistant/components/mysensors/light.py Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof Co-authored-by: Erik Montnemery --- homeassistant/components/mysensors/light.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 81089f052b6..b08d94cebb0 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -132,7 +132,6 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): def _turn_on_rgb_and_w(self, hex_template: str, **kwargs: Any) -> None: """Turn on RGB or RGBW child device.""" assert self._hs - assert self._white is not None rgb = list(color_util.color_hs_to_RGB(*self._hs)) white = self._white hex_color = self._values.get(self.value_type) @@ -151,8 +150,10 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): if hex_template == "%02x%02x%02x%02x": if new_white is not None: rgb.append(new_white) - else: + elif white is not None: rgb.append(white) + else: + rgb.append(0) hex_color = hex_template % tuple(rgb) if len(rgb) > 3: white = rgb.pop() From e2b1cdafc990bc49cf5038a83248fc0ff97d1bb5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Jul 2021 10:43:45 +0200 Subject: [PATCH 139/818] Update frontend to 20210707.0 (#52624) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c7283a9503a..7af6e2bc733 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210706.0" + "home-assistant-frontend==20210707.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c35ff252b52..908cd379886 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210706.0 +home-assistant-frontend==20210707.0 httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index bdd7dd3ba6c..615e7f5ca6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -779,7 +779,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210706.0 +home-assistant-frontend==20210707.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd2157ccc61..176f6a4867e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210706.0 +home-assistant-frontend==20210707.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From b2ee4894f1140733aee97f28208af6875d836b4e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 7 Jul 2021 16:43:19 +0200 Subject: [PATCH 140/818] Bump opencv to 4.5.2.54 (#52630) --- homeassistant/components/opencv/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index e5a82c69aaa..4dcf61636ee 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.0", "opencv-python-headless==4.4.0.42"], + "requirements": ["numpy==1.21.0", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 615e7f5ca6c..c2394ff3c5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1083,7 +1083,7 @@ onvif-zeep-async==1.0.0 open-garage==0.1.4 # homeassistant.components.opencv -# opencv-python-headless==4.4.0.42 +# opencv-python-headless==4.5.2.54 # homeassistant.components.openerz openerz-api==0.1.0 From cd7f36650101a97b0c3014d7fd237c13ea40006c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Betts?= Date: Wed, 7 Jul 2021 17:25:52 +0200 Subject: [PATCH 141/818] Fix service registration typo in Nuki integration (#52631) --- homeassistant/components/nuki/lock.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 9cb1bd01524..25644a49f0f 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -59,6 +59,9 @@ async def async_setup_entry(hass, entry, async_add_entities): vol.Optional(ATTR_UNLATCH, default=False): cv.boolean, }, "lock_n_go", + ) + + platform.async_register_entity_service( "set_continuous_mode", { vol.Required(ATTR_ENABLE): cv.boolean, From 351b67ffb1be95fcfe8618df582bcdf3ef7c7ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 Jul 2021 20:18:43 +0200 Subject: [PATCH 142/818] Handle KeyError when accessing device information (#52650) --- homeassistant/components/ecovacs/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index 964dd7a3f2a..8a6475c0192 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -59,8 +59,8 @@ def setup(hass, config): for device in devices: _LOGGER.info( "Discovered Ecovacs device on account: %s with nickname %s", - device["did"], - device["nick"], + device.get("did"), + device.get("nick"), ) vacbot = VacBot( ecovacs_api.uid, @@ -77,7 +77,8 @@ def setup(hass, config): """Shut down open connections to Ecovacs XMPP server.""" for device in hass.data[ECOVACS_DEVICES]: _LOGGER.info( - "Shutting down connection to Ecovacs device %s", device.vacuum["did"] + "Shutting down connection to Ecovacs device %s", + device.vacuum.get("did"), ) device.disconnect() From 5c827764119c9b2503f44abdea7e40ec776d6abc Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 7 Jul 2021 20:19:31 +0200 Subject: [PATCH 143/818] Fix Fritz default consider home value (#52648) --- homeassistant/components/fritz/common.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index cfa04338db1..a700554d5d9 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -199,12 +199,13 @@ class FritzBoxTools: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) + _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() if self._options: consider_home = self._options.get( - CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + CONF_CONSIDER_HOME, _default_consider_home ) else: - consider_home = DEFAULT_CONSIDER_HOME + consider_home = _default_consider_home new_device = False for known_host in self._update_info(): From 02d8d25d1dd1545ec564b8e91521b78305c78e93 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 7 Jul 2021 23:56:02 +0300 Subject: [PATCH 144/818] Fix Waze Travel Time tests (#52663) --- tests/components/waze_travel_time/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index 237b476aa25..6954522dc85 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -5,6 +5,13 @@ from WazeRouteCalculator import WRCError import pytest +@pytest.fixture(autouse=True) +def mock_wrc(): + """Mock out WazeRouteCalculator.""" + with patch("homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator"): + yield + + @pytest.fixture(name="skip_notifications", autouse=True) def skip_notifications_fixture(): """Skip notification calls.""" From e895b6cd428f02d5afb0e993389c2bb601ca8f92 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 7 Jul 2021 15:29:15 -0700 Subject: [PATCH 145/818] Refactor decompression timestamp validation logic in stream component (#52462) * Refactor dts validation logic into a separate function Create a decompression timestamp validation function to move the logic out of the worker into a separate class. This also uses the python itertools.chain to chain together the initial packets with the remaining packets in the container iterator, removing additional inline if statements. * Reset dts validator when container is reset * Fix typo in a comment * Reuse existing dts_validator when disabling audio stream --- homeassistant/components/stream/worker.py | 105 ++++++++++------------ 1 file changed, 49 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 04be79e668e..ed1e1b9551d 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import deque from collections.abc import Iterator, Mapping from io import BytesIO +import itertools import logging from threading import Event from typing import Any, Callable, cast @@ -201,7 +202,41 @@ class SegmentBuffer: self._memory_file.close() -def stream_worker( # noqa: C901 +class TimestampValidator: + """Validate ordering of timestamps for packets in a stream.""" + + def __init__(self) -> None: + """Initialize the TimestampValidator.""" + # Decompression timestamp of last packet in each stream + self._last_dts: dict[av.stream.Stream, float] = {} + # Number of consecutive missing decompression timestamps + self._missing_dts = 0 + + def is_valid(self, packet: av.Packet) -> float: + """Validate the packet timestamp based on ordering within the stream.""" + # Discard packets missing DTS. Terminate if too many are missing. + if packet.dts is None: + if self._missing_dts >= MAX_MISSING_DTS: + raise StopIteration( + f"No dts in {MAX_MISSING_DTS+1} consecutive packets" + ) + self._missing_dts += 1 + return False + self._missing_dts = 0 + # Discard when dts is not monotonic. Terminate if gap is too wide. + prev_dts = self._last_dts.get(packet.stream, float("-inf")) + if packet.dts <= prev_dts: + gap = packet.time_base * (prev_dts - packet.dts) + if gap > MAX_TIMESTAMP_GAP: + raise StopIteration( + f"Timestamp overflow detected: last dts = {prev_dts}, dts = {packet.dts}" + ) + return False + self._last_dts[packet.stream] = packet.dts + return True + + +def stream_worker( source: str, options: dict[str, str], segment_buffer: SegmentBuffer, @@ -234,10 +269,6 @@ def stream_worker( # noqa: C901 # Iterator for demuxing container_packets: Iterator[av.Packet] - # The decoder timestamps of the latest packet in each stream we processed - last_dts = {video_stream: float("-inf"), audio_stream: float("-inf")} - # Keep track of consecutive packets without a dts to detect end of stream. - missing_dts = 0 # The video dts at the beginning of the segment segment_start_dts: int | None = None # Because of problems 1 and 2 below, we need to store the first few packets and replay them @@ -254,23 +285,17 @@ def stream_worker( # noqa: C901 Also load the first video keyframe dts into segment_start_dts and check if the audio stream really exists. """ nonlocal segment_start_dts, audio_stream, container_packets - missing_dts = 0 found_audio = False try: - container_packets = container.demux((video_stream, audio_stream)) + # Ensure packets are ordered correctly + dts_validator = TimestampValidator() + container_packets = filter( + dts_validator.is_valid, container.demux((video_stream, audio_stream)) + ) first_packet: av.Packet | None = None # Get to first video keyframe while first_packet is None: packet = next(container_packets) - if ( - packet.dts is None - ): # Allow MAX_MISSING_DTS packets with no dts, raise error on the next one - if missing_dts >= MAX_MISSING_DTS: - raise StopIteration( - f"Invalid data - got {MAX_MISSING_DTS+1} packets with missing DTS while initializing" - ) - missing_dts += 1 - continue if packet.stream == audio_stream: found_audio = True elif packet.is_keyframe: # video_keyframe @@ -283,15 +308,6 @@ def stream_worker( # noqa: C901 and len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO ): packet = next(container_packets) - if ( - packet.dts is None - ): # Allow MAX_MISSING_DTS packet with no dts, raise error on the next one - if missing_dts >= MAX_MISSING_DTS: - raise StopIteration( - f"Invalid data - got {MAX_MISSING_DTS+1} packets with missing DTS while initializing" - ) - missing_dts += 1 - continue if packet.stream == audio_stream: # detect ADTS AAC and disable audio if audio_stream.codec.name == "aac" and packet.size > 2: @@ -300,7 +316,10 @@ def stream_worker( # noqa: C901 _LOGGER.warning( "ADTS AAC detected - disabling audio stream" ) - container_packets = container.demux(video_stream) + container_packets = filter( + dts_validator.is_valid, + container.demux(video_stream), + ) audio_stream = None continue found_audio = True @@ -330,42 +349,16 @@ def stream_worker( # noqa: C901 assert isinstance(segment_start_dts, int) segment_buffer.reset(segment_start_dts) + # Rewind the stream and iterate over the initial set of packets again + # filtering out any packets with timestamp ordering issues. + packets = itertools.chain(initial_packets, container_packets) while not quit_event.is_set(): try: - if len(initial_packets) > 0: - packet = initial_packets.popleft() - else: - packet = next(container_packets) - if packet.dts is None: - # Allow MAX_MISSING_DTS consecutive packets without dts. Terminate the stream on the next one. - if missing_dts >= MAX_MISSING_DTS: - raise StopIteration( - f"No dts in {MAX_MISSING_DTS+1} consecutive packets" - ) - missing_dts += 1 - continue - missing_dts = 0 + packet = next(packets) except (av.AVError, StopIteration) as ex: _LOGGER.error("Error demuxing stream: %s", str(ex)) break - # Discard packet if dts is not monotonic - if packet.dts <= last_dts[packet.stream]: - if ( - packet.time_base * (last_dts[packet.stream] - packet.dts) - > MAX_TIMESTAMP_GAP - ): - _LOGGER.warning( - "Timestamp overflow detected: last dts %s, dts = %s, resetting stream", - last_dts[packet.stream], - packet.dts, - ) - break - continue - - # Update last_dts processed - last_dts[packet.stream] = packet.dts - # Mux packets, and possibly write a segment to the output stream. # This mutates packet timestamps and stream segment_buffer.mux_packet(packet) From 50d56fd755c6ab8396f71fe761660b9a6f8ae970 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 7 Jul 2021 17:39:34 -0500 Subject: [PATCH 146/818] Add missing type annotations to Guardian (#52598) --- homeassistant/components/guardian/__init__.py | 8 ++--- .../components/guardian/config_flow.py | 36 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 89ff7768c20..0ff488161ed 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -219,7 +219,7 @@ class GuardianEntity(CoordinatorEntity): self._entry = entry @callback - def _async_update_from_latest_data(self): + def _async_update_from_latest_data(self) -> None: """Update the entity. This should be extended by Guardian platforms. @@ -265,8 +265,8 @@ class ValveControllerEntity(GuardianEntity): coordinators: dict[str, DataUpdateCoordinator], kind: str, name: str, - device_class: str, - icon: str, + device_class: str | None, + icon: str | None, ) -> None: """Initialize.""" super().__init__(entry, kind, name, device_class, icon) @@ -292,7 +292,7 @@ class ValveControllerEntity(GuardianEntity): if coordinator ) - async def _async_continue_entity_setup(self): + async def _async_continue_entity_setup(self) -> None: """Perform additional, internal tasks when the entity is about to be added. This should be extended by Guardian platforms. diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 05be79da344..edbf4ef9c83 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -1,12 +1,18 @@ """Config flow for Elexa Guardian integration.""" +from __future__ import annotations + +from typing import Any + from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_UID, DOMAIN, LOGGER @@ -23,18 +29,18 @@ UNIQUE_ID = "guardian_{0}" @callback -def async_get_pin_from_discovery_hostname(hostname): +def async_get_pin_from_discovery_hostname(hostname: str) -> str: """Get the device's 4-digit PIN from its zeroconf-discovered hostname.""" return hostname.split(".")[0].split("-")[1] @callback -def async_get_pin_from_uid(uid): +def async_get_pin_from_uid(uid: str) -> str: """Get the device's 4-digit PIN from its UID.""" return uid[-4:] -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -52,11 +58,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize.""" self.discovery_info = {} - async def _async_set_unique_id(self, pin): + async def _async_set_unique_id(self, pin: str) -> None: """Set the config entry's unique ID (based on the device's 4-digit PIN).""" await self.async_set_unique_id(UNIQUE_ID.format(pin)) if self.discovery_info: @@ -66,7 +72,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: self._abort_if_unique_id_configured() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle configuration via the UI.""" if user_input is None: return self.async_show_form( @@ -90,7 +98,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input} ) - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle the configuration via dhcp.""" self.discovery_info = { CONF_IP_ADDRESS: discovery_info[IP_ADDRESS], @@ -98,7 +106,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return await self._async_handle_discovery() - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle the configuration via zeroconf.""" self.discovery_info = { CONF_IP_ADDRESS: discovery_info["host"], @@ -108,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_unique_id(pin) return await self._async_handle_discovery() - async def _async_handle_discovery(self): + async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" self.context[CONF_IP_ADDRESS] = self.discovery_info[CONF_IP_ADDRESS] if any( @@ -119,7 +129,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_discovery_confirm(self, user_input=None): + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Finish the configuration via any discovery.""" if user_input is None: self._set_confirm_only() From f44a13970a4cc2c18c423b696bb2dca88f232b6a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 7 Jul 2021 17:39:52 -0500 Subject: [PATCH 147/818] Add missing type annotations to Notion (#52599) --- homeassistant/components/notion/__init__.py | 4 ++-- homeassistant/components/notion/binary_sensor.py | 2 +- homeassistant/components/notion/config_flow.py | 11 ++++++++--- homeassistant/components/notion/sensor.py | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 294d93a30e2..6fff031ae25 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -208,7 +208,7 @@ class NotionEntity(CoordinatorEntity): raise NotImplementedError @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" if self._task_id in self.coordinator.data["tasks"]: self.hass.async_create_task(self._async_update_bridge_id()) @@ -216,7 +216,7 @@ class NotionEntity(CoordinatorEntity): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() self._async_update_from_latest_data() diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 2604e2bdf58..d3b1d8e3ef2 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -44,7 +44,7 @@ BINARY_SENSOR_TYPES = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): +) -> None: """Set up Notion sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 13386a67c02..4b654e4366e 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -1,10 +1,13 @@ """Config flow to configure the Notion integration.""" +from __future__ import annotations + from aionotion import async_get_client from aionotion.errors import NotionError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -15,19 +18,21 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" self.data_schema = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) - async def _show_form(self, errors=None): + async def _show_form(self, errors: dict[str, str] | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", data_schema=self.data_schema, errors=errors or {} ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return await self._show_form() diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 650b884bf27..659b58e9815 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -14,7 +14,7 @@ SENSOR_TYPES = {SENSOR_TEMPERATURE: ("Temperature", "temperature", TEMP_CELSIUS) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): +) -> None: """Set up Notion sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] From aa022d4c526421effdd6ac2d4bd54bad9168085e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 7 Jul 2021 17:40:03 -0500 Subject: [PATCH 148/818] Add missing type annotations to ReCollect Waste (#52600) --- .../components/recollect_waste/__init__.py | 2 +- .../components/recollect_waste/config_flow.py | 15 ++++++++++++--- .../components/recollect_waste/sensor.py | 7 ++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index ce4577321d3..f6a5398d901 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index 9919c2653a8..92f94a314ee 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -1,6 +1,8 @@ """Config flow for ReCollect Waste integration.""" from __future__ import annotations +from typing import Any + from aiorecollect.client import Client from aiorecollect.errors import RecollectError import voluptuous as vol @@ -8,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER @@ -30,11 +33,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return RecollectWasteOptionsFlowHandler(config_entry) - async def async_step_import(self, import_config: dict = None) -> dict: + async def async_step_import( + self, import_config: dict[str, Any] | None = None + ) -> FlowResult: """Handle configuration via YAML import.""" return await self.async_step_user(import_config) - async def async_step_user(self, user_input: dict = None) -> dict: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle configuration via the UI.""" if user_input is None: return self.async_show_form( @@ -77,7 +84,9 @@ class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): """Initialize.""" self._entry = entry - async def async_step_init(self, user_input: dict | None = None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 08fbd449a1b..beb7c182351 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -55,10 +56,10 @@ def async_get_pickup_type_names( async def async_setup_platform( hass: HomeAssistant, - config: dict, + config: ConfigType, async_add_entities: AddEntitiesCallback, - discovery_info: dict = None, -): + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Import Recollect Waste configuration from YAML.""" LOGGER.warning( "Loading ReCollect Waste via platform setup is deprecated; " From f3c464786cbb1ce9b159d36cc21fa001c202d3f5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 7 Jul 2021 18:45:39 -0500 Subject: [PATCH 149/818] Bump simplisafe-python to 11.0.1 (#52684) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index eff37bf1548..02713b106bd 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.0"], + "requirements": ["simplisafe-python==11.0.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c2394ff3c5b..651e5427ca4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2101,7 +2101,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.0 +simplisafe-python==11.0.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 176f6a4867e..42943943f59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1147,7 +1147,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.0 +simplisafe-python==11.0.1 # homeassistant.components.slack slackclient==2.5.0 From b5a6d057783ec498faaceee53578da87326a1303 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 8 Jul 2021 00:13:44 +0000 Subject: [PATCH 150/818] [ci skip] Translation update --- .../components/forecast_solar/translations/nl.json | 12 ++++++++++-- .../components/goalzero/translations/nl.json | 2 +- .../pvpc_hourly_pricing/translations/nl.json | 6 +++--- .../components/simplisafe/translations/zh-Hant.json | 2 +- .../components/zwave_js/translations/nl.json | 3 ++- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/forecast_solar/translations/nl.json b/homeassistant/components/forecast_solar/translations/nl.json index c21f631af7e..c66d272782d 100644 --- a/homeassistant/components/forecast_solar/translations/nl.json +++ b/homeassistant/components/forecast_solar/translations/nl.json @@ -3,10 +3,14 @@ "step": { "user": { "data": { + "azimuth": "Azimut (360 graden, 0 = Noord, 90 = Oost, 180 = Zuid, 270 = West)", + "declination": "Declinatie (0 = Horizontaal, 90 = Verticaal)", "latitude": "Breedtegraad", "longitude": "Lengtegraad", + "modules power": "Totaal Watt piekvermogen van uw zonnepanelen", "name": "Naam" - } + }, + "description": "Vul de gegevens van uw zonnepanelen in. Raadpleeg de documentatie als een veld niet duidelijk is." } } }, @@ -14,7 +18,11 @@ "step": { "init": { "data": { - "api_key": "Forecast.Solar API-sleutel (optioneel)" + "api_key": "Forecast.Solar API-sleutel (optioneel)", + "azimuth": "Azimut (360 graden, 0 = Noord, 90 = Oost, 180 = Zuid, 270 = West)", + "damping": "Dempingsfactor: past de resultaten 's ochtends en 's avonds aan", + "declination": "Declinatie (0 = Horizontaal, 90 = Verticaal)", + "modules power": "Totaal Watt piekvermogen van uw zonnepanelen" }, "description": "Met deze waarden kan het resultaat van Solar.Forecast worden aangepast. Raadpleeg de documentatie als een veld onduidelijk is." } diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index 4f902f8bea2..d73c4d648ed 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -20,7 +20,7 @@ "host": "Host", "name": "Naam" }, - "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om je Yeti te verbinden met je wifi-netwerk. Haal dan de host-ip van uw router. DHCP moet zijn ingesteld in uw routerinstellingen voor het apparaat om ervoor te zorgen dat het host-ip niet verandert. Raadpleeg de gebruikershandleiding van uw router.", + "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om Yeti te verbinden met uw wifi-netwerk. DHCP-reservering op uw router wordt aanbevolen. Als het niet is ingesteld, is het apparaat mogelijk niet meer beschikbaar totdat Home Assistant het nieuwe IP-adres detecteert. Raadpleeg de gebruikershandleiding van uw router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index f74662b06da..b4bc784dedf 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -9,10 +9,10 @@ "name": "Sensornaam", "power": "Gecontracteerd vermogen (kW)", "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", - "tariff": "Gecontracteerd tarief (1, 2 of 3 periodes)" + "tariff": "Toepasselijk tarief per geografische zone" }, - "description": "Deze sensor gebruikt de offici\u00eble API om [uurtarief voor elektriciteit (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanje te krijgen. \n Bezoek voor een meer precieze uitleg de [integratiedocumenten] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Selecteer het gecontracteerde tarief op basis van het aantal factureringsperioden per dag: \n - 1 periode: normaal \n - 2 periodes: discriminatie (nachttarief) \n - 3 periodes: elektrische auto (nachttarief van 3 periodes)", - "title": "Tariefselectie" + "description": "Deze sensor gebruikt de offici\u00eble API om [uurprijs van elektriciteit (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanje te verkrijgen.\n Ga voor een nauwkeurigere uitleg naar de [integratiedocumenten](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Sensor instellen" } } }, diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 27064ed1055..517d48321a8 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -19,7 +19,7 @@ "data": { "password": "\u5bc6\u78bc" }, - "description": "\u5b58\u53d6\u6b0a\u6756\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", + "description": "\u5b58\u53d6\u6b0a\u9650\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 1e37b617c6d..26f75148d57 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -59,7 +59,8 @@ "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.", "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "different_device": "Het aangesloten USB-apparaat is niet hetzelfde als eerder geconfigureerd voor dit configuratie-item. Maak in plaats daarvan een nieuw configuratie-item voor het nieuwe apparaat." }, "error": { "cannot_connect": "Kan geen verbinding maken", From 4e848f60c4b17859f12f2d7ff6c2d8a47d15e292 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 7 Jul 2021 22:12:17 -0400 Subject: [PATCH 151/818] Use entity class attributes for anel_pwrctrl (#52594) * Use entity class attributes for anel_pwrctrl * Tweak --- .../components/anel_pwrctrl/switch.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 0669a3bb6c6..2f4ce0ee7db 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -65,25 +65,13 @@ class PwrCtrlSwitch(SwitchEntity): """Initialize the PwrCtrl switch.""" self._port = port self._parent_device = parent_device - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return f"{self._port.device.host}-{self._port.get_index()}" - - @property - def name(self): - """Return the name of the device.""" - return self._port.label - - @property - def is_on(self): - """Return true if the device is on.""" - return self._port.get_state() + self._attr_unique_id = f"{port.device.host}-{port.get_index()}" + self._attr_name = port.label def update(self): """Trigger update for all switches on the parent device.""" self._parent_device.update() + self._attr_is_on = self._port.get_state() def turn_on(self, **kwargs): """Turn the switch on.""" From 4e85bdd67cb1244eed347ad4fd088a5b62b7c9e3 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 8 Jul 2021 00:11:56 -0700 Subject: [PATCH 152/818] pyWeMo version bump (0.6.5) (#52701) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index bd153294282..3d051fcc6dc 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.6.3"], + "requirements": ["pywemo==0.6.5"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 651e5427ca4..d4f1125f730 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1965,7 +1965,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.3 +pywemo==0.6.5 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42943943f59..8d16f1c967a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1083,7 +1083,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.3 +pywemo==0.6.5 # homeassistant.components.wilight pywilight==0.0.70 From dabb50f7eec7dfde753158841fe14c931c2b79bf Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 8 Jul 2021 02:15:56 -0500 Subject: [PATCH 153/818] Warn if `interface_addr` remains in Sonos configuration (#52652) --- homeassistant/components/sonos/__init__.py | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 218ddaa8e15..ec16ec5bd87 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -52,14 +52,17 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - MP_DOMAIN: vol.Schema( - { - vol.Optional(CONF_ADVERTISE_ADDR): cv.string, - vol.Optional(CONF_INTERFACE_ADDR): cv.string, - vol.Optional(CONF_HOSTS): vol.All( - cv.ensure_list_csv, [cv.string] - ), - } + MP_DOMAIN: vol.All( + cv.deprecated(CONF_INTERFACE_ADDR), + vol.Schema( + { + vol.Optional(CONF_ADVERTISE_ADDR): cv.string, + vol.Optional(CONF_INTERFACE_ADDR): cv.string, + vol.Optional(CONF_HOSTS): vol.All( + cv.ensure_list_csv, [cv.string] + ), + } + ), ) } ) @@ -126,6 +129,13 @@ async def async_setup_entry( # noqa: C901 if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr + if deprecated_address := config.get(CONF_INTERFACE_ADDR): + _LOGGER.warning( + "'%s' is deprecated, enable %s in the Network integration (https://www.home-assistant.io/integrations/network/)", + CONF_INTERFACE_ADDR, + deprecated_address, + ) + async def _async_stop_event_listener(event: Event) -> None: await asyncio.gather( *[speaker.async_unsubscribe() for speaker in data.discovered.values()], From 5b49107007e69d325491ee215183fe5f2fd2b0f8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 8 Jul 2021 03:16:47 -0400 Subject: [PATCH 154/818] Use entity class attributes for avion (#52696) --- homeassistant/components/avion/light.py | 52 +++++-------------------- 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 0d242b952dd..fcc780f77bc 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -65,49 +65,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AvionLight(LightEntity): """Representation of an Avion light.""" + _attr_supported_features = SUPPORT_AVION_LED + _attr_should_poll = False + _attr_assumed_state = True + def __init__(self, device): """Initialize the light.""" - self._name = device.name - self._address = device.mac - self._brightness = 255 - self._state = False + self._attr_name = device.name + self._attr_unique_id = device.mac + self._attr_brightness = 255 self._switch = device - @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_AVION_LED - - @property - def should_poll(self): - """Don't poll.""" - return False - - @property - def assumed_state(self): - """We can't read the actual state, so assume it matches.""" - return True - def set_state(self, brightness): """Set the state of this lamp to the provided brightness.""" avion = importlib.import_module("avion") @@ -130,12 +98,12 @@ class AvionLight(LightEntity): brightness = kwargs.get(ATTR_BRIGHTNESS) if brightness is not None: - self._brightness = brightness + self._attr_brightness = brightness self.set_state(self.brightness) - self._state = True + self._attr_is_on = True def turn_off(self, **kwargs): """Turn the specified or all lights off.""" self.set_state(0) - self._state = False + self._attr_is_on = False From b021e2ee8c911c219d70b284985ec413dc4f1de4 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 8 Jul 2021 00:20:27 -0700 Subject: [PATCH 155/818] Move recorder.py import to runtime (#52682) --- homeassistant/components/stream/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index d8e4cb2cdb2..c7ca853c20c 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -43,7 +43,6 @@ from .const import ( ) from .core import PROVIDERS, IdleTimer, StreamOutput from .hls import async_setup_hls -from .recorder import RecorderOutput _LOGGER = logging.getLogger(__name__) @@ -265,6 +264,10 @@ class Stream: ) -> None: """Make a .mp4 recording from a provided stream.""" + # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel + from .recorder import RecorderOutput + # Check for file access if not self.hass.config.is_allowed_path(video_path): raise HomeAssistantError(f"Can't write {video_path}, no access to path!") From 71b14b51b49d49ef6d87878081731a59f9d58b73 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 8 Jul 2021 03:22:54 -0400 Subject: [PATCH 156/818] Use entity class attributes for aurora_abb_power (#52692) --- .../components/aurora_abb_powerone/sensor.py | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index cd4a71d1b31..9c798b8e6d4 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -51,32 +51,13 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): """Representation of a Sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = POWER_WATT + _attr_device_class = DEVICE_CLASS_POWER def __init__(self, client, name, typename): """Initialize the sensor.""" - self._name = f"{name} {typename}" + self._attr_name = f"{name} {typename}" self.client = client - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return POWER_WATT - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_POWER def update(self): """Fetch new state data for the sensor. @@ -87,8 +68,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): self.client.connect() # read ADC channel 3 (grid power output) power_watts = self.client.measure(3, True) - self._state = round(power_watts, 1) - # _LOGGER.debug("Got reading %fW" % self._state) + self._attr_state = round(power_watts, 1) except AuroraError as error: # aurorapy does not have different exceptions (yet) for dealing # with timeout vs other comms errors. @@ -102,7 +82,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): _LOGGER.debug("No response from inverter (could be dark)") else: raise error - self._state = None + self._attr_state = None finally: if self.client.serline.isOpen(): self.client.close() From cb0a7589ce01e2b024fbb707a59ebf3f4c1f9055 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 8 Jul 2021 09:30:54 +0200 Subject: [PATCH 157/818] Use class properties in netatmo (#52705) --- homeassistant/components/netatmo/camera.py | 2 +- homeassistant/components/netatmo/light.py | 4 ++-- homeassistant/components/netatmo/sensor.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 431aa814bde..798156b7411 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -330,7 +330,7 @@ class NetatmoCamera(NetatmoBase, Camera): async def _service_set_camera_light(self, **kwargs): """Service to set light mode.""" mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) - _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) + _LOGGER.debug("Turn %s camera light for '%s'", mode, self.name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 3ad9db8ae7c..07488ad03b5 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -126,7 +126,7 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_on(self, **kwargs): """Turn camera floodlight on.""" - _LOGGER.debug("Turn camera '%s' on", self._attr_name) + _LOGGER.debug("Turn camera '%s' on", self.name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, @@ -135,7 +135,7 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_off(self, **kwargs): """Turn camera floodlight into auto mode.""" - _LOGGER.debug("Turn camera '%s' to auto mode", self._attr_name) + _LOGGER.debug("Turn camera '%s' to auto mode", self.name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index e8e142d7f16..a6ba3fd1c57 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -366,13 +366,13 @@ class NetatmoSensor(NetatmoBase, SensorEntity): @property def available(self): """Return entity availability.""" - return self._attr_state is not None + return self.state is not None @callback def async_update_callback(self): """Update the entity's state.""" if self._data is None: - if self._attr_state is None: + if self.state is None: return _LOGGER.warning("No data from update") self._attr_state = None @@ -383,7 +383,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): ) if data is None: - if self._attr_state: + if self.state: _LOGGER.debug( "No data found for %s - %s (%s)", self.name, @@ -410,7 +410,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): else: self._attr_state = state except KeyError: - if self._attr_state: + if self.state: _LOGGER.debug("No %s data found for %s", self.type, self._device_name) self._attr_state = None return @@ -614,7 +614,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): data = self._data.get_latest_gust_strengths() if data is None: - if self._attr_state is None: + if self.state is None: return _LOGGER.debug( "No station provides %s data in the area %s", self.type, self._area_name @@ -628,5 +628,5 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): elif self._mode == "max": self._attr_state = max(values) - self._attr_available = self._attr_state is not None + self._attr_available = self.state is not None self.async_write_ha_state() From c4d8d1dc8e2e63497b29242a100fb184f751f37f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 8 Jul 2021 03:39:53 -0400 Subject: [PATCH 158/818] Use entity class attributes for Aten pe (#52687) * Use entity class attributes for aten_pe * Use entity class attributes for atag --- homeassistant/components/aten_pe/switch.py | 42 ++++++---------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 43146938961..044c890fc65 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -61,7 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async for outlet in outlets: switches.append(AtenSwitch(dev, mac, outlet.id, outlet.name)) - async_add_entities(switches) + async_add_entities(switches, True) class AtenSwitch(SwitchEntity): @@ -72,48 +72,26 @@ class AtenSwitch(SwitchEntity): def __init__(self, device, mac, outlet, name): """Initialize an ATEN PE switch.""" self._device = device - self._mac = mac self._outlet = outlet - self._name = name or f"Outlet {outlet}" - self._enabled = False - self._outlet_power = 0.0 - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._mac}-{self._outlet}" - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self._enabled - - @property - def current_power_w(self) -> float: - """Return the current power usage in W.""" - return self._outlet_power + self._attr_unique_id = f"{mac}-{outlet}" + self._attr_name = name or f"Outlet {outlet}" async def async_turn_on(self, **kwargs): """Turn the switch on.""" await self._device.setOutletStatus(self._outlet, "on") - self._enabled = True + self._attr_is_on = True async def async_turn_off(self, **kwargs): """Turn the switch off.""" await self._device.setOutletStatus(self._outlet, "off") - self._enabled = False + self._attr_is_on = False async def async_update(self): """Process update from entity.""" status = await self._device.displayOutletStatus(self._outlet) if status == "on": - self._enabled = True - self._outlet_power = await self._device.outletPower(self._outlet) - elif status == "off": - self._enabled = False - self._outlet_power = 0.0 + self._attr_is_on = True + self._attr_current_power_w = await self._device.outletPower(self._outlet) + else: + self._attr_is_on = False + self._attr_current_power_w = 0.0 From 5ff7c7708d2ef8cb9921e24b7ef99853e3f55429 Mon Sep 17 00:00:00 2001 From: avee87 Date: Thu, 8 Jul 2021 09:01:06 +0100 Subject: [PATCH 159/818] Use iso-formatted times in MetOffice weather forecast (#52672) * Fixed raw datetime in MetOffice weather forecast * Use datetime in sensor attribute --- homeassistant/components/metoffice/weather.py | 2 +- tests/components/metoffice/const.py | 3 +- tests/components/metoffice/test_sensor.py | 18 +- tests/components/metoffice/test_weather.py | 194 +++++++++--------- 4 files changed, 103 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 0b1933c665f..b02539f0e31 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -47,7 +47,7 @@ async def async_setup_entry( def _build_forecast_data(timestep): data = {} - data[ATTR_FORECAST_TIME] = timestep.date + data[ATTR_FORECAST_TIME] = timestep.date.isoformat() if timestep.weather: data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) if timestep.precipitation: diff --git a/tests/components/metoffice/const.py b/tests/components/metoffice/const.py index 5d8d781b042..c9a173e3f12 100644 --- a/tests/components/metoffice/const.py +++ b/tests/components/metoffice/const.py @@ -2,8 +2,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z" -TEST_DATETIME_STRING = "2020-04-25 12:00:00+0000" +TEST_DATETIME_STRING = "2020-04-25T12:00:00+00:00" TEST_API_KEY = "test-metoffice-api-key" diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index e603d0f93f6..201c5922d33 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -6,7 +6,6 @@ from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN from . import NewDateTime from .const import ( - DATETIME_FORMAT, KINGSLYNN_SENSOR_RESULTS, METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, @@ -54,13 +53,10 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim for running_id in running_sensor_ids: sensor = hass.states.get(running_id) sensor_id = sensor.attributes.get("sensor_id") - sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] + _, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] assert sensor.state == sensor_value - assert ( - sensor.attributes.get("last_update").strftime(DATETIME_FORMAT) - == TEST_DATETIME_STRING - ) + assert sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING assert sensor.attributes.get("site_id") == "354107" assert sensor.attributes.get("site_name") == TEST_SITE_NAME_WAVERTREE assert sensor.attributes.get("attribution") == ATTRIBUTION @@ -115,11 +111,10 @@ async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_ti sensor = hass.states.get(running_id) sensor_id = sensor.attributes.get("sensor_id") if sensor.attributes.get("site_id") == "354107": - sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] + _, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] assert sensor.state == sensor_value assert ( - sensor.attributes.get("last_update").strftime(DATETIME_FORMAT) - == TEST_DATETIME_STRING + sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING ) assert sensor.attributes.get("sensor_id") == sensor_id assert sensor.attributes.get("site_id") == "354107" @@ -127,11 +122,10 @@ async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_ti assert sensor.attributes.get("attribution") == ATTRIBUTION else: - sensor_name, sensor_value = KINGSLYNN_SENSOR_RESULTS[sensor_id] + _, sensor_value = KINGSLYNN_SENSOR_RESULTS[sensor_id] assert sensor.state == sensor_value assert ( - sensor.attributes.get("last_update").strftime(DATETIME_FORMAT) - == TEST_DATETIME_STRING + sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING ) assert sensor.attributes.get("sensor_id") == sensor_id assert sensor.attributes.get("site_id") == "322380" diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 6d4187c7023..21b2196804c 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -9,7 +9,6 @@ from homeassistant.util import utcnow from . import NewDateTime from .const import ( - DATETIME_FORMAT, METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, WAVERTREE_SENSOR_RESULTS, @@ -74,11 +73,11 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - entity = hass.states.get("weather.met_office_wavertree_3_hourly") - assert entity + weather = hass.states.get("weather.met_office_wavertree_3_hourly") + assert weather - entity = hass.states.get("weather.met_office_wavertree_daily") - assert entity + weather = hass.states.get("weather.met_office_wavertree_daily") + assert weather requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="") @@ -87,11 +86,11 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): async_fire_time_changed(hass, future_time) await hass.async_block_till_done() - entity = hass.states.get("weather.met_office_wavertree_3_hourly") - assert entity.state == STATE_UNAVAILABLE + weather = hass.states.get("weather.met_office_wavertree_3_hourly") + assert weather.state == STATE_UNAVAILABLE - entity = hass.states.get("weather.met_office_wavertree_daily") - assert entity.state == STATE_UNAVAILABLE + weather = hass.states.get("weather.met_office_wavertree_daily") + assert weather.state == STATE_UNAVAILABLE @patch( @@ -126,50 +125,49 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti await hass.async_block_till_done() # Wavertree 3-hourly weather platform expected results - entity = hass.states.get("weather.met_office_wavertree_3_hourly") - assert entity + weather = hass.states.get("weather.met_office_wavertree_3_hourly") + assert weather - assert entity.state == "sunny" - assert entity.attributes.get("temperature") == 17 - assert entity.attributes.get("wind_speed") == 9 - assert entity.attributes.get("wind_bearing") == "SSE" - assert entity.attributes.get("visibility") == "Good - 10-20" - assert entity.attributes.get("humidity") == 50 + assert weather.state == "sunny" + assert weather.attributes.get("temperature") == 17 + assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_bearing") == "SSE" + assert weather.attributes.get("visibility") == "Good - 10-20" + assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check - assert len(entity.attributes.get("forecast")) == 35 + assert len(weather.attributes.get("forecast")) == 35 assert ( - entity.attributes.get("forecast")[26]["datetime"].strftime(DATETIME_FORMAT) - == "2020-04-28 21:00:00+0000" + weather.attributes.get("forecast")[26]["datetime"] + == "2020-04-28T21:00:00+00:00" ) - assert entity.attributes.get("forecast")[26]["condition"] == "cloudy" - assert entity.attributes.get("forecast")[26]["temperature"] == 10 - assert entity.attributes.get("forecast")[26]["wind_speed"] == 4 - assert entity.attributes.get("forecast")[26]["wind_bearing"] == "NNE" + assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[26]["temperature"] == 10 + assert weather.attributes.get("forecast")[26]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" # Wavertree daily weather platform expected results - entity = hass.states.get("weather.met_office_wavertree_daily") - assert entity + weather = hass.states.get("weather.met_office_wavertree_daily") + assert weather - assert entity.state == "sunny" - assert entity.attributes.get("temperature") == 19 - assert entity.attributes.get("wind_speed") == 9 - assert entity.attributes.get("wind_bearing") == "SSE" - assert entity.attributes.get("visibility") == "Good - 10-20" - assert entity.attributes.get("humidity") == 50 + assert weather.state == "sunny" + assert weather.attributes.get("temperature") == 19 + assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_bearing") == "SSE" + assert weather.attributes.get("visibility") == "Good - 10-20" + assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(entity.attributes.get("forecast")) == 8 + assert len(weather.attributes.get("forecast")) == 8 assert ( - entity.attributes.get("forecast")[7]["datetime"].strftime(DATETIME_FORMAT) - == "2020-04-29 12:00:00+0000" + weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert entity.attributes.get("forecast")[7]["condition"] == "rainy" - assert entity.attributes.get("forecast")[7]["temperature"] == 13 - assert entity.attributes.get("forecast")[7]["wind_speed"] == 13 - assert entity.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[7]["condition"] == "rainy" + assert weather.attributes.get("forecast")[7]["temperature"] == 13 + assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" @patch( @@ -216,93 +214,91 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t await hass.async_block_till_done() # Wavertree 3-hourly weather platform expected results - entity = hass.states.get("weather.met_office_wavertree_3_hourly") - assert entity + weather = hass.states.get("weather.met_office_wavertree_3_hourly") + assert weather - assert entity.state == "sunny" - assert entity.attributes.get("temperature") == 17 - assert entity.attributes.get("wind_speed") == 9 - assert entity.attributes.get("wind_bearing") == "SSE" - assert entity.attributes.get("visibility") == "Good - 10-20" - assert entity.attributes.get("humidity") == 50 + assert weather.state == "sunny" + assert weather.attributes.get("temperature") == 17 + assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_bearing") == "SSE" + assert weather.attributes.get("visibility") == "Good - 10-20" + assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check - assert len(entity.attributes.get("forecast")) == 35 + assert len(weather.attributes.get("forecast")) == 35 assert ( - entity.attributes.get("forecast")[18]["datetime"].strftime(DATETIME_FORMAT) - == "2020-04-27 21:00:00+0000" + weather.attributes.get("forecast")[18]["datetime"] + == "2020-04-27T21:00:00+00:00" ) - assert entity.attributes.get("forecast")[18]["condition"] == "sunny" - assert entity.attributes.get("forecast")[18]["temperature"] == 9 - assert entity.attributes.get("forecast")[18]["wind_speed"] == 4 - assert entity.attributes.get("forecast")[18]["wind_bearing"] == "NW" + assert weather.attributes.get("forecast")[18]["condition"] == "sunny" + assert weather.attributes.get("forecast")[18]["temperature"] == 9 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" # Wavertree daily weather platform expected results - entity = hass.states.get("weather.met_office_wavertree_daily") - assert entity + weather = hass.states.get("weather.met_office_wavertree_daily") + assert weather - assert entity.state == "sunny" - assert entity.attributes.get("temperature") == 19 - assert entity.attributes.get("wind_speed") == 9 - assert entity.attributes.get("wind_bearing") == "SSE" - assert entity.attributes.get("visibility") == "Good - 10-20" - assert entity.attributes.get("humidity") == 50 + assert weather.state == "sunny" + assert weather.attributes.get("temperature") == 19 + assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_bearing") == "SSE" + assert weather.attributes.get("visibility") == "Good - 10-20" + assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(entity.attributes.get("forecast")) == 8 + assert len(weather.attributes.get("forecast")) == 8 assert ( - entity.attributes.get("forecast")[7]["datetime"].strftime(DATETIME_FORMAT) - == "2020-04-29 12:00:00+0000" + weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert entity.attributes.get("forecast")[7]["condition"] == "rainy" - assert entity.attributes.get("forecast")[7]["temperature"] == 13 - assert entity.attributes.get("forecast")[7]["wind_speed"] == 13 - assert entity.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[7]["condition"] == "rainy" + assert weather.attributes.get("forecast")[7]["temperature"] == 13 + assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results - entity = hass.states.get("weather.met_office_king_s_lynn_3_hourly") - assert entity + weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") + assert weather - assert entity.state == "sunny" - assert entity.attributes.get("temperature") == 14 - assert entity.attributes.get("wind_speed") == 2 - assert entity.attributes.get("wind_bearing") == "E" - assert entity.attributes.get("visibility") == "Very Good - 20-40" - assert entity.attributes.get("humidity") == 60 + assert weather.state == "sunny" + assert weather.attributes.get("temperature") == 14 + assert weather.attributes.get("wind_speed") == 2 + assert weather.attributes.get("wind_bearing") == "E" + assert weather.attributes.get("visibility") == "Very Good - 20-40" + assert weather.attributes.get("humidity") == 60 # Also has Forecast added - just pick out 1 entry to check - assert len(entity.attributes.get("forecast")) == 35 + assert len(weather.attributes.get("forecast")) == 35 assert ( - entity.attributes.get("forecast")[18]["datetime"].strftime(DATETIME_FORMAT) - == "2020-04-27 21:00:00+0000" + weather.attributes.get("forecast")[18]["datetime"] + == "2020-04-27T21:00:00+00:00" ) - assert entity.attributes.get("forecast")[18]["condition"] == "cloudy" - assert entity.attributes.get("forecast")[18]["temperature"] == 10 - assert entity.attributes.get("forecast")[18]["wind_speed"] == 7 - assert entity.attributes.get("forecast")[18]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[18]["temperature"] == 10 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" # King's Lynn daily weather platform expected results - entity = hass.states.get("weather.met_office_king_s_lynn_daily") - assert entity + weather = hass.states.get("weather.met_office_king_s_lynn_daily") + assert weather - assert entity.state == "cloudy" - assert entity.attributes.get("temperature") == 9 - assert entity.attributes.get("wind_speed") == 4 - assert entity.attributes.get("wind_bearing") == "ESE" - assert entity.attributes.get("visibility") == "Very Good - 20-40" - assert entity.attributes.get("humidity") == 75 + assert weather.state == "cloudy" + assert weather.attributes.get("temperature") == 9 + assert weather.attributes.get("wind_speed") == 4 + assert weather.attributes.get("wind_bearing") == "ESE" + assert weather.attributes.get("visibility") == "Very Good - 20-40" + assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check - assert len(entity.attributes.get("forecast")) == 8 + assert len(weather.attributes.get("forecast")) == 8 assert ( - entity.attributes.get("forecast")[5]["datetime"].strftime(DATETIME_FORMAT) - == "2020-04-28 12:00:00+0000" + weather.attributes.get("forecast")[5]["datetime"] == "2020-04-28T12:00:00+00:00" ) - assert entity.attributes.get("forecast")[5]["condition"] == "cloudy" - assert entity.attributes.get("forecast")[5]["temperature"] == 11 - assert entity.attributes.get("forecast")[5]["wind_speed"] == 7 - assert entity.attributes.get("forecast")[5]["wind_bearing"] == "ESE" + assert weather.attributes.get("forecast")[5]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[5]["temperature"] == 11 + assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" From 62c7e5408b14e80952920b8dfb6664edbb29363c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 8 Jul 2021 10:09:30 +0200 Subject: [PATCH 160/818] Ensure Forecast.Solar returns an iso formatted timestamp (#52669) --- homeassistant/components/forecast_solar/sensor.py | 6 +++++- tests/components/forecast_solar/test_sensor.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index a6b1927926e..b32f1f341be 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -1,6 +1,8 @@ """Support for the Forecast.Solar sensor service.""" from __future__ import annotations +from datetime import datetime + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME @@ -64,5 +66,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): @property def state(self) -> StateType: """Return the state of the sensor.""" - state: StateType = getattr(self.coordinator.data, self._sensor.key) + state: StateType | datetime = getattr(self.coordinator.data, self._sensor.key) + if isinstance(state, datetime): + return state.isoformat() return state diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index a3513b86a5d..31c367678c1 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -70,7 +70,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" - assert state.state == "2021-06-27 13:00:00+00:00" + assert state.state == "2021-06-27T13:00:00+00:00" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP @@ -82,7 +82,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" - assert state.state == "2021-06-27 14:00:00+00:00" + assert state.state == "2021-06-27T14:00:00+00:00" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" ) From abca7deadbb04230a383e40cf068a15210a2856d Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Thu, 8 Jul 2021 10:42:27 +0200 Subject: [PATCH 161/818] Hint for str type instead of explicitly casting to str (#52712) --- homeassistant/components/lcn/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 740319417c3..a1451f339f2 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,6 +1,8 @@ """Support for LCN sensors.""" from __future__ import annotations +from typing import cast + import pypck from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR, SensorEntity @@ -97,7 +99,7 @@ class LcnVariableSensor(LcnEntity, SensorEntity): @property def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return str(self.unit.value) + return cast(str, self.unit.value) def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" From 1dd4ba5fcdb2b5d622ec6c0e01b106f417c9186b Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 8 Jul 2021 11:39:56 +0200 Subject: [PATCH 162/818] Fix precipitation calculation for hourly forecast (#52676) It seems that hourly forecast have precipitation in 3h blocks. --- .../components/openweathermap/weather_update_coordinator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 98f39290d22..73edc9fae75 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -191,6 +191,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): """Get rain data from weather data.""" if "all" in rain: return round(rain["all"], 2) + if "3h" in rain: + return round(rain["3h"], 2) if "1h" in rain: return round(rain["1h"], 2) return 0 @@ -201,6 +203,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): if snow: if "all" in snow: return round(snow["all"], 2) + if "3h" in snow: + return round(snow["3h"], 2) if "1h" in snow: return round(snow["1h"], 2) return 0 From 7d0751df8a4f1b47e597dadfbe42031e11403409 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 8 Jul 2021 05:42:07 -0400 Subject: [PATCH 163/818] Use entity class attributes for anthemav (#52602) --- .../components/anthemav/media_player.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 788fa8db7eb..18b2e704538 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -82,11 +82,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AnthemAVR(MediaPlayerEntity): """Entity reading values from Anthem AVR protocol.""" + _attr_should_poll = False + _attr_supported_features = SUPPORT_ANTHEMAV + def __init__(self, avr, name): """Initialize entity with transport.""" super().__init__() self.avr = avr - self._name = name + self._attr_name = name or self._lookup("model") def _lookup(self, propname, dval=None): return getattr(self.avr.protocol, propname, dval) @@ -97,21 +100,6 @@ class AnthemAVR(MediaPlayerEntity): async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) ) - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_ANTHEMAV - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return name of device.""" - return self._name or self._lookup("model") - @property def state(self): """Return state of power on/off.""" From 1c11b247e4b232dd79950337c497aac3bd9712c7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 8 Jul 2021 05:55:26 -0400 Subject: [PATCH 164/818] Use entity class attributes for apcupsd (#52662) --- .../components/apcupsd/binary_sensor.py | 15 +------- homeassistant/components/apcupsd/sensor.py | 37 ++++--------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index daf9592f3e6..8a1f98329bb 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -25,20 +25,9 @@ class OnlineStatus(BinarySensorEntity): def __init__(self, config, data): """Initialize the APCUPSd binary device.""" - self._config = config self._data = data - self._state = None - - @property - def name(self): - """Return the name of the UPS online status sensor.""" - return self._config[CONF_NAME] - - @property - def is_on(self): - """Return true if the UPS is online, else false.""" - return self._state & VALUE_ONLINE > 0 + self._attr_name = config[CONF_NAME] def update(self): """Get the status report from APCUPSd and set this entity's state.""" - self._state = int(self._data.status[KEY_STATUS], 16) + self._attr_is_on = int(self._data.status[KEY_STATUS], 16) & VALUE_ONLINE > 0 diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 36dc1155b7f..190a3f5f1d8 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -162,39 +162,18 @@ class APCUPSdSensor(SensorEntity): """Initialize the sensor.""" self._data = data self.type = sensor_type - self._name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0] - self._unit = SENSOR_TYPES[sensor_type][1] - self._inferred_unit = None - self._state = None - - @property - def name(self): - """Return the name of the UPS sensor.""" - return self._name - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] - - @property - def state(self): - """Return true if the UPS is online, else False.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - if not self._unit: - return self._inferred_unit - return self._unit + self._attr_name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0] + self._attr_icon = SENSOR_TYPES[self.type][2] + if SENSOR_TYPES[sensor_type][1]: + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] def update(self): """Get the latest status and use it to update our sensor state.""" if self.type.upper() not in self._data.status: - self._state = None - self._inferred_unit = None + self._attr_state = None else: - self._state, self._inferred_unit = infer_unit( + self._attr_state, inferred_unit = infer_unit( self._data.status[self.type.upper()] ) + if not self._attr_unit_of_measurement: + self._attr_unit_of_measurement = inferred_unit From 578c8971616ff7f7b22d3e3d10db4ed02cbfa01d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 8 Jul 2021 04:56:50 -0500 Subject: [PATCH 165/818] Ignore unused keys from Sonos device properties callback (#52660) * Ignore known but unused keys from device callback * Fix bug, add test --- homeassistant/components/sonos/speaker.py | 5 +++++ tests/components/sonos/test_sensor.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ec59d946c09..14adbc337fb 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -71,6 +71,7 @@ SUBSCRIPTION_SERVICES = [ "zoneGroupTopology", ] UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} +UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] _LOGGER = logging.getLogger(__name__) @@ -407,6 +408,10 @@ class SonosSpeaker: """Update device properties from an event.""" if more_info := event.variables.get("more_info"): battery_dict = dict(x.split(":") for x in more_info.split(",")) + for unused in UNUSED_DEVICE_KEYS: + battery_dict.pop(unused, None) + if not battery_dict: + return if "BattChg" not in battery_dict: _LOGGER.debug( "Unknown device properties update for %s (%s), please report an issue: '%s'", diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 8d402b589b0..80f050fe6fc 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -103,3 +103,23 @@ async def test_device_payload_without_battery( await hass.async_block_till_done() assert bad_payload in caplog.text + + +async def test_device_payload_without_battery_and_ignored_keys( + hass, config_entry, config, soco, battery_event, caplog +): + """Test device properties event update without battery info and ignored keys.""" + soco.get_battery_info.return_value = None + + await setup_platform(hass, config_entry, config) + + subscription = soco.deviceProperties.subscribe.return_value + sub_callback = subscription.callback + + ignored_payload = "SPID:InCeiling,TargetRoomName:Bouncy House" + battery_event.variables["more_info"] = ignored_payload + + sub_callback(battery_event) + await hass.async_block_till_done() + + assert ignored_payload not in caplog.text From fe1f7ba31635dc6bc353dc1873aefcafa2429fe8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Jul 2021 11:58:51 +0200 Subject: [PATCH 166/818] Add check for _client existence in modbus (#52719) --- .coveragerc | 1 + homeassistant/components/modbus/modbus.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 8ba0356fa9c..3c7f7640623 100644 --- a/.coveragerc +++ b/.coveragerc @@ -633,6 +633,7 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* homeassistant/components/modbus/climate.py + homeassistant/components/modbus/modbus.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 2e5892dbf1d..0826f4d5794 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -310,6 +310,8 @@ class ModbusHub: """Convert async to sync pymodbus call.""" if self._config_delay: return None + if not self._client: + return None if not self._client.is_socket_open(): return None async with self._lock: From 94e15b3eea59a3691d5905801368905720d4ead1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 Jul 2021 00:44:49 +1200 Subject: [PATCH 167/818] Esphome fix camera image (#52738) --- homeassistant/components/esphome/camera.py | 4 ++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 7afd89bf9be..f047d5c1bdd 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -77,7 +77,7 @@ class EsphomeCamera(Camera, EsphomeBaseEntity): await self._image_cond.wait() if not self.available: return None - return self._state.image[:] + return self._state.data[:] async def _async_camera_stream_image(self) -> bytes | None: """Return a single camera image in a stream.""" @@ -88,7 +88,7 @@ class EsphomeCamera(Camera, EsphomeBaseEntity): await self._image_cond.wait() if not self.available: return None - return self._state.image[:] + return self._state.data[:] async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e69299a4a43..e48cd4847c8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==4.0.1"], + "requirements": ["aioesphomeapi==5.0.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index d4f1125f730..8dd3f4896db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==4.0.1 +aioesphomeapi==5.0.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d16f1c967a..5afedac092a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==4.0.1 +aioesphomeapi==5.0.0 # homeassistant.components.flo aioflo==0.4.1 From f069fbdb2519cce073d83ab3f6b9b88bd83df0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Thu, 8 Jul 2021 14:45:34 +0200 Subject: [PATCH 168/818] Upgrade Fronius dependency to 0.5.3 (#52737) supports more values of new Gen24 type of fronius device --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index d526fc90b32..1ae95d30fd5 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -2,7 +2,7 @@ "domain": "fronius", "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.5.2"], + "requirements": ["pyfronius==0.5.3"], "codeowners": ["@nielstron"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 8dd3f4896db..09db4fb5c32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1449,7 +1449,7 @@ pyfreedompro==1.1.0 pyfritzhome==0.6.2 # homeassistant.components.fronius -pyfronius==0.5.2 +pyfronius==0.5.3 # homeassistant.components.ifttt pyfttt==0.3 From 293690e3d82f65da2e520fbab2b08bfd70fccb4a Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Thu, 8 Jul 2021 15:05:43 +0200 Subject: [PATCH 169/818] Fix KNX Fan features (#52732) * Fan entity should return support features * Revert "Fan entity should return support features" This reverts commit 3ad0e87708fbf1847aaa26e3bc76fcac365a1640. * Restore supported_features for KNX fan --- homeassistant/components/knx/fan.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index f787795e1e8..21ede700fdd 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -66,9 +66,6 @@ class KNXFan(KnxEntity, FanEntity): # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None - self._attr_supported_features = SUPPORT_SET_SPEED - if self._device.supports_oscillation: - self._attr_supported_features |= SUPPORT_OSCILLATE self._attr_unique_id = str(self._device.speed.group_address) async def async_set_percentage(self, percentage: int) -> None: @@ -79,6 +76,16 @@ class KNXFan(KnxEntity, FanEntity): else: await self._device.set_speed(percentage) + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = SUPPORT_SET_SPEED + + if self._device.supports_oscillation: + flags |= SUPPORT_OSCILLATE + + return flags + @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" From 922ef3f2f386da0210fdad1d3adbfe198c3f80ee Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 8 Jul 2021 09:06:00 -0400 Subject: [PATCH 170/818] Use entity class attributes for aurora (#52690) * Use entity class attributes for aurora * fix --- homeassistant/components/aurora/__init__.py | 30 +++++---------------- homeassistant/components/aurora/sensor.py | 7 ++--- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index faccefda500..576ccc1275b 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -123,6 +123,8 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): class AuroraEntity(CoordinatorEntity): """Implementation of the base Aurora Entity.""" + _attr_extra_state_attributes = {"attribution": ATTRIBUTION} + def __init__( self, coordinator: AuroraDataUpdateCoordinator, @@ -133,35 +135,15 @@ class AuroraEntity(CoordinatorEntity): super().__init__(coordinator=coordinator) - self._name = name - self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}" - self._icon = icon - - @property - def unique_id(self): - """Define the unique id based on the latitude and longitude.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {"attribution": ATTRIBUTION} - - @property - def icon(self): - """Return the icon for the sensor.""" - return self._icon + self._attr_name = name + self._attr_unique_id = f"{coordinator.latitude}_{coordinator.longitude}" + self._attr_icon = icon @property def device_info(self): """Define the device based on name.""" return { - ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, + ATTR_IDENTIFIERS: {(DOMAIN, self.unique_id)}, ATTR_NAME: self.coordinator.name, ATTR_MANUFACTURER: "NOAA", ATTR_MODEL: "Aurora Visibility Sensor", diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index d7024cc630a..76be6ca97f8 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -22,12 +22,9 @@ async def async_setup_entry(hass, entry, async_add_entries): class AuroraSensor(AuroraEntity, SensorEntity): """Implementation of an aurora sensor.""" + _attr_unit_of_measurement = PERCENTAGE + @property def state(self): """Return % chance the aurora is visible.""" return self.coordinator.data - - @property - def unit_of_measurement(self): - """Return the unit of measure.""" - return PERCENTAGE From eb735b616213eed7815d973b97cebbf49b4a6122 Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Thu, 8 Jul 2021 08:18:08 -0500 Subject: [PATCH 171/818] Bump pylutron to 0.2.8 fixing python 3.9 incompatibility (#52702) --- homeassistant/components/lutron/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index db1c9090ce8..83c4ee72345 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron", "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", - "requirements": ["pylutron==0.2.7"], + "requirements": ["pylutron==0.2.8"], "codeowners": ["@JonGilmore"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 09db4fb5c32..1a2b89864a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1561,7 +1561,7 @@ pyloopenergy==0.2.1 pylutron-caseta==0.10.0 # homeassistant.components.lutron -pylutron==0.2.7 +pylutron==0.2.8 # homeassistant.components.mailgun pymailgunner==1.4 From 49181d6ba8bc1fb4d9160947c78589a9f11d4355 Mon Sep 17 00:00:00 2001 From: apaperclip <67401560+apaperclip@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:10:05 -0400 Subject: [PATCH 172/818] Remove scale calculation for climacell cloud cover (#52752) --- homeassistant/components/climacell/const.py | 1 - homeassistant/components/climacell/weather.py | 2 -- tests/components/climacell/test_sensor.py | 2 +- tests/components/climacell/test_weather.py | 4 ++-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 062de93375b..057cef5e993 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -214,7 +214,6 @@ CC_SENSOR_TYPES = [ ATTR_FIELD: CC_ATTR_CLOUD_COVER, ATTR_NAME: "Cloud Cover", CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_SCALE: 1 / 100, }, { ATTR_FIELD: CC_ATTR_WIND_GUST, diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 1b3b50e8566..be03b53ef72 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -207,8 +207,6 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4 ) cloud_cover = self.cloud_cover - if cloud_cover is not None: - cloud_cover /= 100 return { ATTR_CLOUD_COVER: cloud_cover, ATTR_WIND_GUST: wind_gust, diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index d93bdb5fae8..c642457b63e 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -182,7 +182,7 @@ async def test_v4_sensor( check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.9688") check_sensor_state(hass, GHI, "0.0") check_sensor_state(hass, CLOUD_BASE, "1.1909") - check_sensor_state(hass, CLOUD_COVER, "1.0") + check_sensor_state(hass, CLOUD_COVER, "100") check_sensor_state(hass, CLOUD_CEILING, "1.1909") check_sensor_state(hass, WIND_GUST, "5.6506") check_sensor_state(hass, PRECIPITATION_TYPE, "rain") diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index fa1ef9dc490..90efdea3c8c 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -228,7 +228,7 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289 - assert weather_state.attributes[ATTR_CLOUD_COVER] == 1 + assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" @@ -391,6 +391,6 @@ async def test_v4_weather( assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152 - assert weather_state.attributes[ATTR_CLOUD_COVER] == 1 + assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 assert weather_state.attributes[ATTR_WIND_GUST] == 20.3421 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" From 5b257d2be80430819513874e8e247fb05a90c8f8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 8 Jul 2021 17:26:25 +0100 Subject: [PATCH 173/818] Fix homebridge devices becoming unavailable frequently (#52753) Update to aiohomekit 0.4.3 and make sure service type UUID is normalised before comparison Co-authored-by: J. Nick Koston --- .../components/homekit_controller/manifest.json | 2 +- homeassistant/components/homekit_controller/sensor.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/test_sensor.py | 10 ++++++++++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 496d629d112..2d40cc8a235 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.4.2"], + "requirements": ["aiohomekit==0.4.3"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index fe98b75130c..b21010e9b1e 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -44,7 +44,8 @@ SIMPLE_SENSOR = { "unit": TEMP_CELSIUS, # This sensor is only for temperature characteristics that are not part # of a temperature sensor service. - "probe": lambda char: char.service.type != ServicesTypes.TEMPERATURE_SENSOR, + "probe": lambda char: char.service.type + != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), }, } diff --git a/requirements_all.txt b/requirements_all.txt index 1a2b89864a9..b7f01f0edef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.2 +aiohomekit==0.4.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5afedac092a..34c2d899d23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.2 +aiohomekit==0.4.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index a79e94c4bb7..604c83e54f7 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -86,6 +86,16 @@ async def test_temperature_sensor_read_state(hass, utcnow): assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE +async def test_temperature_sensor_not_added_twice(hass, utcnow): + """A standalone temperature sensor should not get a characteristic AND a service entity.""" + helper = await setup_test_component( + hass, create_temperature_sensor_service, suffix="temperature" + ) + + for state in hass.states.async_all(): + assert state.entity_id == helper.entity_id + + async def test_humidity_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit humidity sensor accessory.""" helper = await setup_test_component( From f7e4db512f05dab32dcb4cb0fe1218d790c2b736 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 9 Jul 2021 00:09:30 +0000 Subject: [PATCH 174/818] [ci skip] Translation update --- .../components/airvisual/translations/he.json | 9 +++++ .../alarm_control_panel/translations/it.json | 4 ++ .../binary_sensor/translations/ar.json | 3 ++ .../cloudflare/translations/it.json | 7 ++++ .../components/coinbase/translations/ar.json | 1 + .../components/coinbase/translations/it.json | 40 +++++++++++++++++++ .../demo/translations/select.ar.json | 7 ++++ .../devolo_home_control/translations/it.json | 6 ++- .../forecast_solar/translations/ar.json | 11 +++++ .../forecast_solar/translations/it.json | 31 ++++++++++++++ .../freedompro/translations/ar.json | 3 -- .../freedompro/translations/fr.json | 9 +++++ .../freedompro/translations/it.json | 20 ++++++++++ .../components/hyperion/translations/ar.json | 10 +++++ .../components/isy994/translations/ar.json | 12 ++++++ .../components/kraken/translations/ar.json | 11 +++++ .../components/light/translations/ar.json | 13 ++++++ .../components/luftdaten/translations/he.json | 7 ++++ .../components/motioneye/translations/fr.json | 10 +++++ .../components/motioneye/translations/it.json | 10 +++++ .../components/netatmo/translations/he.json | 9 +++++ .../nmap_tracker/translations/ar.json | 16 +++++++- .../nmap_tracker/translations/fr.json | 12 ++++++ .../nmap_tracker/translations/he.json | 4 +- .../nmap_tracker/translations/it.json | 40 +++++++++++++++++++ .../components/onvif/translations/ar.json | 4 +- .../components/onvif/translations/it.json | 13 ++++++ .../openweathermap/translations/he.json | 6 ++- .../philips_js/translations/it.json | 9 +++++ .../simplisafe/translations/it.json | 2 +- .../components/tile/translations/nl.json | 4 +- .../components/upnp/translations/ar.json | 11 +++++ .../components/weather/translations/ar.json | 2 + .../components/wilight/translations/ar.json | 15 +++++++ .../components/wled/translations/ar.json | 11 +++++ .../components/yeelight/translations/ar.json | 20 ++++++++++ .../components/zha/translations/ar.json | 3 ++ .../components/zwave_js/translations/ar.json | 9 +++++ 38 files changed, 399 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/it.json create mode 100644 homeassistant/components/demo/translations/select.ar.json create mode 100644 homeassistant/components/forecast_solar/translations/ar.json create mode 100644 homeassistant/components/forecast_solar/translations/it.json create mode 100644 homeassistant/components/freedompro/translations/fr.json create mode 100644 homeassistant/components/freedompro/translations/it.json create mode 100644 homeassistant/components/hyperion/translations/ar.json create mode 100644 homeassistant/components/isy994/translations/ar.json create mode 100644 homeassistant/components/kraken/translations/ar.json create mode 100644 homeassistant/components/nmap_tracker/translations/fr.json create mode 100644 homeassistant/components/nmap_tracker/translations/it.json create mode 100644 homeassistant/components/upnp/translations/ar.json create mode 100644 homeassistant/components/wilight/translations/ar.json create mode 100644 homeassistant/components/wled/translations/ar.json create mode 100644 homeassistant/components/yeelight/translations/ar.json create mode 100644 homeassistant/components/zwave_js/translations/ar.json diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index 3a9e5a1cb18..6d5684220aa 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -35,5 +35,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u05d4\u05e6\u05d2 \u05d2\u05d9\u05d0\u05d5\u05d2\u05e8\u05e4\u05d9\u05d4 \u05de\u05e0\u05d5\u05d8\u05e8\u05ea \u05d1\u05de\u05e4\u05d4" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/it.json b/homeassistant/components/alarm_control_panel/translations/it.json index 1574f88541b..2015dc0e09d 100644 --- a/homeassistant/components/alarm_control_panel/translations/it.json +++ b/homeassistant/components/alarm_control_panel/translations/it.json @@ -4,6 +4,7 @@ "arm_away": "Armare {entity_name} uscito", "arm_home": "Armare {entity_name} casa", "arm_night": "Armare {entity_name} notte", + "arm_vacation": "Armare {entity_name} vacanza", "disarm": "Disarmare {entity_name}", "trigger": "Attivazione {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} \u00e8 attivo in modalit\u00e0 fuori casa", "is_armed_home": "{entity_name} \u00e8 attivo in modalit\u00e0 a casa", "is_armed_night": "{entity_name} \u00e8 attivo in modalit\u00e0 notte", + "is_armed_vacation": "{entity_name} \u00e8 attivo in modalit\u00e0 vacanza", "is_disarmed": "{entity_name} \u00e8 disattivo", "is_triggered": "{entity_name} \u00e8 attivato" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} attivato in modalit\u00e0 fuori casa", "armed_home": "{entity_name} attivato in modalit\u00e0 a casa", "armed_night": "{entity_name} attivato in modalit\u00e0 notte", + "armed_vacation": "{entity_name} attivato in modalit\u00e0 vacanza", "disarmed": "{entity_name} disattivato", "triggered": "{entity_name} attivato" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Attivo con bypass personalizzato", "armed_home": "Attivo in casa", "armed_night": "Attivo Notte", + "armed_vacation": "Attivo Vacanza", "arming": "In Attivazione", "disarmed": "Disattivo", "disarming": "In Disattivazione", diff --git a/homeassistant/components/binary_sensor/translations/ar.json b/homeassistant/components/binary_sensor/translations/ar.json index 7782421ef1c..0d835aea3f3 100644 --- a/homeassistant/components/binary_sensor/translations/ar.json +++ b/homeassistant/components/binary_sensor/translations/ar.json @@ -32,6 +32,9 @@ "off": "\u0637\u0628\u064a\u0639\u064a", "on": "\u062d\u0627\u0631" }, + "light": { + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641 \u0639\u0646 \u0627\u0644\u0636\u0648\u0621" + }, "lock": { "off": "\u0645\u0642\u0641\u0644", "on": "\u063a\u064a\u0631 \u0645\u0642\u0641\u0644" diff --git a/homeassistant/components/cloudflare/translations/it.json b/homeassistant/components/cloudflare/translations/it.json index df5c045dd99..fae00a790dc 100644 --- a/homeassistant/components/cloudflare/translations/it.json +++ b/homeassistant/components/cloudflare/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "unknown": "Errore imprevisto" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "Token API", + "description": "Eseguire nuovamente l'autenticazione con l'account Cloudflare." + } + }, "records": { "data": { "records": "Record" diff --git a/homeassistant/components/coinbase/translations/ar.json b/homeassistant/components/coinbase/translations/ar.json index 402aaadbd63..30655126631 100644 --- a/homeassistant/components/coinbase/translations/ar.json +++ b/homeassistant/components/coinbase/translations/ar.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_token": "\u0633\u0631 API", "exchange_rates": "\u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641" }, "description": "\u064a\u0631\u062c\u0649 \u0625\u062f\u062e\u0627\u0644 \u062a\u0641\u0627\u0635\u064a\u0644 \u0645\u0641\u062a\u0627\u062d API \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0639\u0644\u0649 \u0627\u0644\u0646\u062d\u0648 \u0627\u0644\u0645\u0646\u0635\u0648\u0635 \u0639\u0644\u064a\u0647 \u0645\u0646 \u0642\u0628\u0644 Coinbase.", diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json new file mode 100644 index 00000000000..b45f263ef37 --- /dev/null +++ b/homeassistant/components/coinbase/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "api_token": " API Segreta", + "currencies": "Valute del saldo del conto", + "exchange_rates": "Tassi di cambio" + }, + "description": "Inserisci i dettagli della tua chiave API come forniti da Coinbase.", + "title": "Dettagli della chiave API di Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Uno o pi\u00f9 saldi in valuta richiesti non sono forniti dalla tua API Coinbase.", + "exchange_rate_unavaliable": "Uno o pi\u00f9 dei tassi di cambio richiesti non sono forniti da Coinbase.", + "unknown": "Errore imprevisto" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Saldi del portafoglio da segnalare.", + "exchange_rate_currencies": "Tassi di cambio da segnalare." + }, + "description": "Regolare le opzioni di Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.ar.json b/homeassistant/components/demo/translations/select.ar.json new file mode 100644 index 00000000000..c151c29acfb --- /dev/null +++ b/homeassistant/components/demo/translations/select.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "demo__speed": { + "light_speed": "\u0633\u0631\u0639\u0629 \u0627\u0644\u0636\u0648\u0621" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json index bb19fde73a9..1197702ae2a 100644 --- a/homeassistant/components/devolo_home_control/translations/it.json +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "invalid_auth": "Autenticazione non valida" + "invalid_auth": "Autenticazione non valida", + "reauth_failed": "Si prega di utilizzare lo stesso utente mydevolo di prima." }, "step": { "user": { diff --git a/homeassistant/components/forecast_solar/translations/ar.json b/homeassistant/components/forecast_solar/translations/ar.json new file mode 100644 index 00000000000..1c213149988 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "modules power": "\u0625\u062c\u0645\u0627\u0644\u064a \u0637\u0627\u0642\u0629 \u0630\u0631\u0648\u0629 \u0648\u0627\u0637 \u0644\u0648\u062d\u062f\u0627\u062a \u0627\u0644\u0637\u0627\u0642\u0629 \u0627\u0644\u0634\u0645\u0633\u064a\u0629 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/it.json b/homeassistant/components/forecast_solar/translations/it.json new file mode 100644 index 00000000000..1f7f7677888 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimut (360 gradi, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ovest)", + "declination": "Declinazione (0 = Orizzontale, 90 = Verticale)", + "latitude": "Latitudine", + "longitude": "Logitudine", + "modules power": "Potenza di picco totale in Watt dei tuoi moduli solari", + "name": "Nome" + }, + "description": "Compila i dati dei tuoi pannelli solari. Fare riferimento alla documentazione se un campo non \u00e8 chiaro." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Chiave API Forecast.Solar (opzionale)", + "azimuth": "Azimut (360 gradi, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ovest)", + "damping": "Fattore di smorzamento: regola i risultati al mattino e alla sera", + "declination": "Declinazione (0 = Orizzontale, 90 = Verticale)", + "modules power": "Potenza di picco totale in Watt dei tuoi moduli solari" + }, + "description": "Questi valori consentono di modificare il risultato di Solar.Forecast. Fare riferimento alla documentazione se un campo non \u00e8 chiaro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/ar.json b/homeassistant/components/freedompro/translations/ar.json index 87f48ee498d..799f812ecca 100644 --- a/homeassistant/components/freedompro/translations/ar.json +++ b/homeassistant/components/freedompro/translations/ar.json @@ -2,9 +2,6 @@ "config": { "step": { "user": { - "data": { - "api_key": "\u0645\u0641\u062a\u0627\u062d API" - }, "description": "\u0627\u0644\u0631\u062c\u0627\u0621 \u0625\u062f\u062e\u0627\u0644 \u0645\u0641\u062a\u0627\u062d API \u0627\u0644\u0630\u064a \u062a\u0645 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u064a\u0647 \u0645\u0646 https://home.freedompro.eu", "title": "\u0645\u0641\u062a\u0627\u062d Freedompro API" } diff --git a/homeassistant/components/freedompro/translations/fr.json b/homeassistant/components/freedompro/translations/fr.json new file mode 100644 index 00000000000..4822faaef86 --- /dev/null +++ b/homeassistant/components/freedompro/translations/fr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Cl\u00e9 API Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/it.json b/homeassistant/components/freedompro/translations/it.json new file mode 100644 index 00000000000..51dfa372f17 --- /dev/null +++ b/homeassistant/components/freedompro/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API" + }, + "description": "Inserisci la chiave API ottenuta da https://home.freedompro.eu", + "title": "Chiave API Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ar.json b/homeassistant/components/hyperion/translations/ar.json new file mode 100644 index 00000000000..9a017f7b2b2 --- /dev/null +++ b/homeassistant/components/hyperion/translations/ar.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0636\u0627\u0641\u0629 Hyperion Ambilight \u0625\u0644\u0649 Home Assistant\u061f \n\n ** \u0627\u0644\u0645\u0636\u064a\u0641: ** {host}\n ** \u0627\u0644\u0645\u0646\u0641\u0630: ** {port}\n ** \u0627\u0644\u0645\u0639\u0631\u0641 **: {id}", + "title": "\u062a\u0623\u0643\u064a\u062f \u0625\u0636\u0627\u0641\u0629 \u062e\u062f\u0645\u0629 Hyperion Ambilight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ar.json b/homeassistant/components/isy994/translations/ar.json new file mode 100644 index 00000000000..e3805ee46f8 --- /dev/null +++ b/homeassistant/components/isy994/translations/ar.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "restore_light_state": "\u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0633\u0637\u0648\u0639 \u0627\u0644\u0636\u0648\u0621" + }, + "description": "\u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a \u0644\u0644\u062a\u0643\u0627\u0645\u0644 ISY: \n \u2022 \u0633\u0644\u0633\u0644\u0629 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0639\u0642\u062f\u0629: \u0623\u064a \u062c\u0647\u0627\u0632 \u0623\u0648 \u0645\u062c\u0644\u062f \u064a\u062d\u062a\u0648\u064a \u0639\u0644\u0649 \"\u0633\u0644\u0633\u0644\u0629 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0639\u0642\u062f\u0629\" \u0641\u064a \u0627\u0644\u0627\u0633\u0645 \u0633\u064a\u062a\u0645 \u0627\u0644\u062a\u0639\u0627\u0645\u0644 \u0645\u0639\u0647\u0627 \u0639\u0644\u0649 \u0623\u0646\u0647\u0627 \u062c\u0647\u0627\u0632 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0623\u0648 \u062c\u0647\u0627\u0632 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u062b\u0646\u0627\u0626\u064a. \n \u2022 \u062a\u062c\u0627\u0647\u0644 \u0627\u0644\u0633\u0644\u0633\u0644\u0629: \u0633\u064a\u062a\u0645 \u062a\u062c\u0627\u0647\u0644 \u0623\u064a \u062c\u0647\u0627\u0632 \u0645\u0639 '\u062a\u062c\u0627\u0647\u0644 \u0633\u0644\u0633\u0644\u0629' \u0641\u064a \u0627\u0644\u0627\u0633\u0645. \n \u2022 \u0633\u0644\u0633\u0644\u0629 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0645\u062a\u063a\u064a\u0631: \u0623\u064a \u0645\u062a\u063a\u064a\u0631 \u064a\u062d\u062a\u0648\u064a \u0639\u0644\u0649 \"\u0633\u0644\u0633\u0644\u0629 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0645\u062a\u063a\u064a\u0631\" \u0633\u064a\u062a\u0645 \u0625\u0636\u0627\u0641\u062a\u0647\u0627 \u0643\u0645\u0633\u062a\u0634\u0639\u0631. \n \u2022 \u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0633\u0637\u0648\u0639 \u0627\u0644\u0636\u0648\u0621: \u0625\u0630\u0627 \u062a\u0645 \u062a\u0645\u0643\u064a\u0646\u0647\u060c \u0633\u064a\u062a\u0645 \u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0627\u0644\u0633\u0637\u0648\u0639 \u0627\u0644\u0633\u0627\u0628\u0642 \u0639\u0646\u062f \u062a\u0634\u063a\u064a\u0644 \u0636\u0648\u0621 \u0628\u062f\u0644\u0627 \u0645\u0646 \u0627\u0644\u0645\u062f\u0645\u062c \u0641\u064a \u0627\u0644\u062c\u0647\u0627\u0632 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u062a\u0648\u0649." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/ar.json b/homeassistant/components/kraken/translations/ar.json new file mode 100644 index 00000000000..2c2b892acee --- /dev/null +++ b/homeassistant/components/kraken/translations/ar.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0641\u062a\u0631\u0629 \u062a\u0643\u0631\u0627\u0631 \u0627\u0644\u062a\u062d\u062f\u064a\u062b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/ar.json b/homeassistant/components/light/translations/ar.json index 23554bd603b..3b54e253bec 100644 --- a/homeassistant/components/light/translations/ar.json +++ b/homeassistant/components/light/translations/ar.json @@ -1,4 +1,17 @@ { + "device_automation": { + "action_type": { + "flash": "\u0641\u0644\u0627\u0634 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0642\u064a\u062f \u0627\u0644\u0627\u064a\u0642\u0627\u0641", + "is_on": "{entity_name} \u0642\u064a\u062f \u0627\u0644\u062a\u0634\u063a\u064a\u0644" + }, + "trigger_type": { + "turned_off": "{entity_name} \u062a\u0645 \u0625\u064a\u0642\u0627\u0641 \u062a\u0634\u063a\u064a\u0644\u0647", + "turned_on": "{entity_name} \u062a\u0645 \u062a\u0634\u063a\u064a\u0644\u0647" + } + }, "state": { "_": { "off": "\u0625\u064a\u0642\u0627\u0641", diff --git a/homeassistant/components/luftdaten/translations/he.json b/homeassistant/components/luftdaten/translations/he.json index 11a4c93b42a..fba5b8e0492 100644 --- a/homeassistant/components/luftdaten/translations/he.json +++ b/homeassistant/components/luftdaten/translations/he.json @@ -3,6 +3,13 @@ "error": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "show_on_map": "\u05d4\u05e6\u05d2 \u05d1\u05de\u05e4\u05d4" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index a520c05dba2..338bc392bd5 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -16,5 +16,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Configurer les webhooks motionEye pour signaler les \u00e9v\u00e9nements \u00e0 Home Assistant", + "webhook_set_overwrite": "\u00c9craser les webhooks non reconnus" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json index 27e1167b3db..77307be07dd 100644 --- a/homeassistant/components/motioneye/translations/it.json +++ b/homeassistant/components/motioneye/translations/it.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Configura i webhooks di motionEye per segnalare gli eventi a Home Assistant", + "webhook_set_overwrite": "Sovrascrivi webhook non riconosciuti" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json index 814a0093b2c..54571f698fe 100644 --- a/homeassistant/components/netatmo/translations/he.json +++ b/homeassistant/components/netatmo/translations/he.json @@ -26,5 +26,14 @@ "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } + }, + "options": { + "step": { + "public_weather": { + "data": { + "show_on_map": "\u05d4\u05e6\u05d2 \u05d1\u05de\u05e4\u05d4" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ar.json b/homeassistant/components/nmap_tracker/translations/ar.json index abe5450c018..9f223966416 100644 --- a/homeassistant/components/nmap_tracker/translations/ar.json +++ b/homeassistant/components/nmap_tracker/translations/ar.json @@ -1,4 +1,17 @@ { + "config": { + "error": { + "invalid_hosts": "\u0627\u0644\u0645\u0636\u064a\u0641\u0648\u0646 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u064a\u0646" + }, + "step": { + "user": { + "data": { + "home_interval": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0644\u0639\u062f\u062f \u0627\u0644\u062f\u0642\u0627\u0626\u0642 \u0628\u064a\u0646 \u0645\u0633\u062d \u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u0646\u0634\u0637\u0629 (\u0644\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629)", + "hosts": "\u0639\u0646\u0627\u0648\u064a\u0646 \u0627\u0644\u0634\u0628\u0643\u0629 (\u0645\u0641\u0635\u0648\u0644\u0629 \u0628\u0641\u0648\u0627\u0635\u0644) \u0644\u0644\u0645\u0633\u062d" + } + } + } + }, "options": { "step": { "init": { @@ -8,5 +21,6 @@ } } } - } + }, + "title": "Nmap \u0627\u0644\u0645\u0642\u062a\u0641\u064a" } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/fr.json b/homeassistant/components/nmap_tracker/translations/fr.json new file mode 100644 index 00000000000..f4302a4173c --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/fr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Intervalle d\u2019analyse", + "track_new_devices": "Suivre les nouveaux appareils" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/he.json b/homeassistant/components/nmap_tracker/translations/he.json index 0296c7103b6..d57ca363944 100644 --- a/homeassistant/components/nmap_tracker/translations/he.json +++ b/homeassistant/components/nmap_tracker/translations/he.json @@ -28,7 +28,9 @@ "exclude": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e4\u05e1\u05d9\u05e7) \u05e9\u05dc\u05d0 \u05d9\u05d9\u05db\u05dc\u05dc\u05d5 \u05d1\u05e1\u05e8\u05d9\u05e7\u05d4", "home_interval": "\u05de\u05e1\u05e4\u05e8 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9 \u05e9\u05dc \u05d3\u05e7\u05d5\u05ea \u05d1\u05d9\u05df \u05e1\u05e8\u05d9\u05e7\u05d5\u05ea \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e4\u05e2\u05d9\u05dc\u05d9\u05dd (\u05e9\u05d9\u05de\u05d5\u05e8 \u05e1\u05d5\u05dc\u05dc\u05d4)", "hosts": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7) \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", - "scan_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e1\u05e8\u05d9\u05e7\u05d4 \u05d2\u05d5\u05dc\u05de\u05d9\u05d5\u05ea \u05d4\u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 Nmap" + "interval_seconds": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", + "scan_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e1\u05e8\u05d9\u05e7\u05d4 \u05d2\u05d5\u05dc\u05de\u05d9\u05d5\u05ea \u05d4\u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 Nmap", + "track_new_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05d3\u05e9\u05d9\u05dd" }, "description": "\u05e7\u05d1\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05e9\u05d9\u05e1\u05e8\u05e7\u05d5 \u05e2\u05dc \u05d9\u05d3\u05d9 Nmap. \u05db\u05ea\u05d5\u05d1\u05ea \u05e8\u05e9\u05ea \u05d5\u05d0\u05d9 \u05d4\u05db\u05dc\u05dc\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05d4\u05d9\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP (192.168.1.1), \u05e8\u05e9\u05ea\u05d5\u05ea IP (192.168.0.0/24) \u05d0\u05d5 \u05d8\u05d5\u05d5\u05d7\u05d9 IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/it.json b/homeassistant/components/nmap_tracker/translations/it.json new file mode 100644 index 00000000000..921d131c3bb --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "invalid_hosts": "Host non validi" + }, + "step": { + "user": { + "data": { + "exclude": "Indirizzi di rete (separati da virgole) da escludere dalla scansione", + "home_interval": "Numero minimo di minuti tra le scansioni dei dispositivi attivi (preserva la batteria)", + "hosts": "Indirizzi di rete (separati da virgole) da scansionare", + "scan_options": "Opzioni di scansione configurabili non elaborate per Nmap" + }, + "description": "Configura gli host da scansionare con Nmap. L'indirizzo di rete e le esclusioni possono essere indirizzi IP (192.168.1.1), reti IP (192.168.0.0/24) o intervalli IP (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Host non validi" + }, + "step": { + "init": { + "data": { + "exclude": "Indirizzi di rete (separati da virgole) da escludere dalla scansione", + "home_interval": "Numero minimo di minuti tra le scansioni dei dispositivi attivi (preserva la batteria)", + "hosts": "Indirizzi di rete (separati da virgole) da scansionare", + "interval_seconds": "Intervallo di scansione", + "scan_options": "Opzioni di scansione configurabili non elaborate per Nmap", + "track_new_devices": "Traccia nuovi dispositivi" + }, + "description": "Configura gli host da scansionare con Nmap. L'indirizzo di rete e le esclusioni possono essere indirizzi IP (192.168.1.1), reti IP (192.168.0.0/24) o intervalli IP (192.168.1.0-32)." + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ar.json b/homeassistant/components/onvif/translations/ar.json index fb1023da658..5135d42e795 100644 --- a/homeassistant/components/onvif/translations/ar.json +++ b/homeassistant/components/onvif/translations/ar.json @@ -5,9 +5,7 @@ "data": { "host": "\u0627\u0644\u0645\u0636\u064a\u0641", "name": "\u0627\u0644\u0627\u0633\u0645", - "password": "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631", - "port": "\u0627\u0644\u0645\u0646\u0641\u0630", - "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645" + "password": "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631" }, "title": "\u062a\u0643\u0648\u064a\u0646 \u062c\u0647\u0627\u0632 ONVIF" }, diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json index 118af410974..eea178f990a 100644 --- a/homeassistant/components/onvif/translations/it.json +++ b/homeassistant/components/onvif/translations/it.json @@ -18,6 +18,16 @@ }, "title": "Configurare l'autenticazione" }, + "configure": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Configura il dispositivo ONVIF" + }, "configure_profile": { "data": { "include": "Crea entit\u00e0 telecamera" @@ -40,6 +50,9 @@ "title": "Configurare il dispositivo ONVIF" }, "user": { + "data": { + "auto": "Cerca automaticamente" + }, "description": "Facendo clic su Invia, cercheremo nella tua rete i dispositivi ONVIF che supportano il profilo S. \n\nAlcuni produttori hanno iniziato a disabilitare ONVIF per impostazione predefinita. Assicurati che ONVIF sia abilitato nella configurazione della tua telecamera.", "title": "Configurazione del dispositivo ONVIF" } diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json index 554fefee321..ca5e388ea98 100644 --- a/homeassistant/components/openweathermap/translations/he.json +++ b/homeassistant/components/openweathermap/translations/he.json @@ -14,8 +14,10 @@ "language": "\u05e9\u05e4\u05d4", "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", - "mode": "\u05de\u05e6\u05d1" - } + "mode": "\u05de\u05e6\u05d1", + "name": "\u05e9\u05dd \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "title": "\u05de\u05e4\u05ea OpenWeather" } } }, diff --git a/homeassistant/components/philips_js/translations/it.json b/homeassistant/components/philips_js/translations/it.json index 6ff668dbea8..1e08d052c63 100644 --- a/homeassistant/components/philips_js/translations/it.json +++ b/homeassistant/components/philips_js/translations/it.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Si richiede l'accensione del dispositivo" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Consenti l'utilizzo del servizio di notifica dei dati." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 13e7fe4562a..37671d75917 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "L'accesso \u00e8 scaduto o revocato. Inserisci la password per ri-collegare il tuo account.", + "description": "Il tuo accesso \u00e8 scaduto o revocato. Inserisci la password per ricollegare il tuo account.", "title": "Autenticare nuovamente l'integrazione" }, "user": { diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index 236d250122a..2f81919fc4b 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -12,7 +12,7 @@ "password": "Wachtwoord", "username": "E-mail" }, - "title": "Tegel configureren" + "title": "Configureer Tile" } } }, @@ -22,7 +22,7 @@ "data": { "show_inactive": "Toon inactieve tegels" }, - "title": "Tegel configureren" + "title": "Configureer Tile" } } } diff --git a/homeassistant/components/upnp/translations/ar.json b/homeassistant/components/upnp/translations/ar.json new file mode 100644 index 00000000000..b1767243354 --- /dev/null +++ b/homeassistant/components/upnp/translations/ar.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0641\u062a\u0631\u0629 \u062a\u0643\u0631\u0627\u0631 \u0627\u0644\u062a\u062d\u062f\u064a\u062b (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a \u060c 30 \u0639\u0644\u0649 \u0627\u0644\u0623\u0642\u0644)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/ar.json b/homeassistant/components/weather/translations/ar.json index c6e2e316556..89e6cddf113 100644 --- a/homeassistant/components/weather/translations/ar.json +++ b/homeassistant/components/weather/translations/ar.json @@ -3,6 +3,8 @@ "_": { "cloudy": "Bewolkt", "fog": "Mist", + "lightning": "\u0628\u0631\u0642", + "lightning-rainy": "\u0628\u0631\u0642 \u060c \u0645\u0627\u0637\u0631", "sunny": "\u0645\u0634\u0645\u0633" } } diff --git a/homeassistant/components/wilight/translations/ar.json b/homeassistant/components/wilight/translations/ar.json new file mode 100644 index 00000000000..033cc81d349 --- /dev/null +++ b/homeassistant/components/wilight/translations/ar.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_supported_device": "\u0647\u0630\u0627 WiLight \u063a\u064a\u0631 \u0645\u062f\u0639\u0648\u0645 \u062d\u0627\u0644\u064a\u0627", + "not_wilight_device": "\u0647\u0630\u0627 \u0627\u0644\u062c\u0647\u0627\u0632 \u0644\u064a\u0633 WiLight" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f WiLight {name} \u061f \n\n \u064a\u062f\u0639\u0645: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ar.json b/homeassistant/components/wled/translations/ar.json new file mode 100644 index 00000000000..65a38b0c21d --- /dev/null +++ b/homeassistant/components/wled/translations/ar.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "\u062d\u0627\u0641\u0638 \u0639\u0644\u0649 \u0627\u0644\u0636\u0648\u0621 \u0627\u0644\u0631\u0626\u064a\u0633\u064a\u060c \u062d\u062a\u0649 \u0645\u0639 \u0642\u0637\u0639\u0629 LED \u0648\u0627\u062d\u062f\u0629." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ar.json b/homeassistant/components/yeelight/translations/ar.json new file mode 100644 index 00000000000..e4146138625 --- /dev/null +++ b/homeassistant/components/yeelight/translations/ar.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "{model} {host}", + "step": { + "discovery_confirm": { + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f {model} ( {host} )\u061f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "use_music_mode": "\u062a\u0645\u0643\u064a\u0646 \u0648\u0636\u0639 \u0627\u0644\u0645\u0648\u0633\u064a\u0642\u0649" + }, + "description": "\u0625\u0630\u0627 \u062a\u0631\u0643\u062a \u0627\u0644\u0646\u0645\u0648\u0630\u062c \u0641\u0627\u0631\u063a\u064b\u0627 \u060c \u0641\u0633\u064a\u062a\u0645 \u0627\u0643\u062a\u0634\u0627\u0641\u0647 \u062a\u0644\u0642\u0627\u0626\u064a\u064b\u0627." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ar.json b/homeassistant/components/zha/translations/ar.json index e62253baf49..cdc300391e4 100644 --- a/homeassistant/components/zha/translations/ar.json +++ b/homeassistant/components/zha/translations/ar.json @@ -2,6 +2,9 @@ "config_panel": { "zha_alarm_options": { "title": "\u062e\u064a\u0627\u0631\u0627\u062a \u0644\u0648\u062d\u0629 \u0627\u0644\u062a\u062d\u0643\u0645 \u0641\u064a \u0627\u0644\u0625\u0646\u0630\u0627\u0631" + }, + "zha_options": { + "default_light_transition": "\u0648\u0642\u062a \u0627\u0646\u062a\u0642\u0627\u0644 \u0627\u0644\u0636\u0648\u0621 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a)" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ar.json b/homeassistant/components/zwave_js/translations/ar.json new file mode 100644 index 00000000000..65eac260b9f --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ar.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "on_supervisor": { + "title": "\u062d\u062f\u062f \u0637\u0631\u064a\u0642\u0629 \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + } + } + } +} \ No newline at end of file From 2eb531b8c8f1994665ad1a0e9b89879a72331f09 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 9 Jul 2021 01:55:26 -0400 Subject: [PATCH 175/818] Upgrade pymazda to 0.2.0 (#52775) --- homeassistant/components/mazda/__init__.py | 6 ++++-- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mazda/test_init.py | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index d704cfb7f44..921dd4c06c5 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -50,7 +50,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: region = entry.data[CONF_REGION] websession = aiohttp_client.async_get_clientsession(hass) - mazda_client = MazdaAPI(email, password, region, websession) + mazda_client = MazdaAPI( + email, password, region, websession=websession, use_cached_vehicle_list=True + ) try: await mazda_client.validate_credentials() @@ -166,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER, name=DOMAIN, update_method=async_update_data, - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=180), ) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index dd169159bc8..cc12653f5cb 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.1.6"], + "requirements": ["pymazda==0.2.0"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index b7f01f0edef..2efde4e9213 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1570,7 +1570,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.1.6 +pymazda==0.2.0 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34c2d899d23..66a6423b88d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -887,7 +887,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.1.6 +pymazda==0.2.0 # homeassistant.components.melcloud pymelcloud==2.5.3 diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 0c47ae8f2e0..8b135f15e80 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -97,7 +97,7 @@ async def test_update_auth_failure(hass: HomeAssistant): "homeassistant.components.mazda.MazdaAPI.get_vehicles", side_effect=MazdaAuthenticationException("Login failed"), ): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181)) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() @@ -136,7 +136,7 @@ async def test_update_general_failure(hass: HomeAssistant): "homeassistant.components.mazda.MazdaAPI.get_vehicles", side_effect=Exception("Unknown exception"), ): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181)) await hass.async_block_till_done() entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage") From 07e2a7245fabeb961c9b4cca15f6dd6aaa90d05c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 9 Jul 2021 11:38:38 +0200 Subject: [PATCH 176/818] Fix ESPHome Camera not merging image packets (#52783) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e48cd4847c8..d8a22534001 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==5.0.0"], + "requirements": ["aioesphomeapi==5.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 2efde4e9213..44052d425bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.0.0 +aioesphomeapi==5.0.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66a6423b88d..2af2982e1c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.0.0 +aioesphomeapi==5.0.1 # homeassistant.components.flo aioflo==0.4.1 From 7e2ef8f0c7a4911469db89843a669517ada212d1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 9 Jul 2021 10:51:46 +0100 Subject: [PATCH 177/818] Support certain homekit devices that emit invalid JSON (#52759) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 2d40cc8a235..816ec2db4d9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.4.3"], + "requirements": ["aiohomekit==0.5.0"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 44052d425bc..6274f1cb125 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.3 +aiohomekit==0.5.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2af2982e1c8..73fedd92dd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.3 +aiohomekit==0.5.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 2787dc9e97eff418e88674a8bb84b558fab4214b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 9 Jul 2021 11:54:40 +0200 Subject: [PATCH 178/818] Bump dependency to properly handle current and voltage not being reported on some zhapower endpoints (#52764) --- homeassistant/components/deconz/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index ad57b1bd903..fbd420e4b16 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,13 +3,17 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==80"], + "requirements": [ + "pydeconz==81" + ], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], - "codeowners": ["@Kane610"], + "codeowners": [ + "@Kane610" + ], "quality_scale": "platinum", "iot_class": "local_push" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 6274f1cb125..085c7584dd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1374,7 +1374,7 @@ pydaikin==2.4.4 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==80 +pydeconz==81 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73fedd92dd1..47f01a0b962 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -769,7 +769,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.4.4 # homeassistant.components.deconz -pydeconz==80 +pydeconz==81 # homeassistant.components.dexcom pydexcom==0.2.0 From 1e6229dd7b270ace5b3c8bbe1991e1630d0bb100 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Fri, 9 Jul 2021 16:36:13 +0200 Subject: [PATCH 179/818] Add device info to Freedompro (#52715) * Update Freedompro * Update homeassistant/components/freedompro/light.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/freedompro/light.py | 8 ++++++++ tests/components/freedompro/const.py | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index ca96dba00f7..4b39fa395d1 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -43,6 +43,14 @@ class Device(CoordinatorEntity, LightEntity): self._attr_unique_id = device["uid"] self._type = device["type"] self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": self._type, + "manufacturer": "Freedompro", + } self._attr_is_on = False self._attr_brightness = 0 color_mode = COLOR_MODE_ONOFF diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py index 8635858d000..6cf67c932dc 100644 --- a/tests/components/freedompro/const.py +++ b/tests/components/freedompro/const.py @@ -15,7 +15,7 @@ DEVICES = [ }, { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS", - "name": "Bedroom fan", + "name": "bedroom", "type": "fan", "characteristics": ["on", "rotationSpeed"], }, @@ -39,7 +39,7 @@ DEVICES = [ }, { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W", - "name": "Irrigation switch", + "name": "irrigation", "type": "switch", "characteristics": ["on"], }, @@ -81,7 +81,7 @@ DEVICES = [ }, { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI", - "name": "Bedroom thermostat", + "name": "thermostat", "type": "thermostat", "characteristics": [ "heatingCoolingState", @@ -91,7 +91,7 @@ DEVICES = [ }, { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", - "name": "Bedroom window covering", + "name": "blind", "type": "windowCovering", "characteristics": ["position"], }, From 92ab471f7bcbdf4cf4ced11a8f33a641d2303acd Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 9 Jul 2021 09:15:20 -0700 Subject: [PATCH 180/818] Add transition support to zwave_js lights (#52160) * Add transition support to zwave_js lights * Add transition support to color_switch lights * simplify and add tests * fix logic * add check for color transition to add SUPPORT_TRANSITON supported features * Use new metadata property * Use new metadata property * update tests and device state dump json files * fix file perms * update tests and fixtures with new metadata * update test * update test * update tests for color transitions * check for color tansitions as well * more tests * fix color transtions * remove unneed default * set add_to_watched_value_ids to false * set transition default * properly set default * update tests * make sure transition is an int * suggested changes * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * formatting Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/light.py | 107 +++++++++-------- tests/components/zwave_js/test_light.py | 113 ++++++++++++++++++ .../zwave_js/bulb_6_multi_color_state.json | 26 +++- .../light_color_null_values_state.json | 3 + tests/fixtures/zwave_js/zen_31_state.json | 15 +++ 5 files changed, 214 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index b50f2231f46..748d199e93f 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -44,6 +44,8 @@ MULTI_COLOR_MAP = { ColorComponent.PURPLE: "purple", } +TRANSITION_DURATION = "transitionDuration" + async def async_setup_entry( hass: HomeAssistant, @@ -109,8 +111,13 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._supported_color_modes = set() # get additional (optional) values and set features - self._target_value = self.get_zwave_value("targetValue") - self._dimming_duration = self.get_zwave_value("duration") + self._target_brightness = self.get_zwave_value( + "targetValue", add_to_watched_value_ids=False + ) + self._target_color = self.get_zwave_value( + "targetColor", CommandClass.SWITCH_COLOR, add_to_watched_value_ids=False + ) + self._calculate_color_values() if self._supports_rgbw: self._supported_color_modes.add(COLOR_MODE_RGBW) @@ -123,7 +130,17 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # Entity class attributes self._attr_supported_features = 0 - if self._dimming_duration is not None: + self.supports_brightness_transition = bool( + self._target_brightness is not None + and TRANSITION_DURATION + in self._target_brightness.metadata.value_change_options + ) + self.supports_color_transition = bool( + self._target_color is not None + and TRANSITION_DURATION in self._target_color.metadata.value_change_options + ) + + if self.supports_brightness_transition or self.supports_color_transition: self._attr_supported_features |= SUPPORT_TRANSITION @callback @@ -183,6 +200,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" + + transition = kwargs.get(ATTR_TRANSITION) + # RGB/HS color hs_color = kwargs.get(ATTR_HS_COLOR) if hs_color is not None and self._supports_color: @@ -196,7 +216,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # turn of white leds when setting rgb colors[ColorComponent.WARM_WHITE] = 0 colors[ColorComponent.COLD_WHITE] = 0 - await self._async_set_colors(colors) + await self._async_set_colors(colors, transition) # Color temperature color_temp = kwargs.get(ATTR_COLOR_TEMP) @@ -222,7 +242,8 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): ColorComponent.BLUE: 0, ColorComponent.WARM_WHITE: warm, ColorComponent.COLD_WHITE: cold, - } + }, + transition, ) # RGBW @@ -238,18 +259,18 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): if self._cold_white: rgbw_channels[ColorComponent.COLD_WHITE] = rgbw[3] - await self._async_set_colors(rgbw_channels) + await self._async_set_colors(rgbw_channels, transition) # set brightness - await self._async_set_brightness( - kwargs.get(ATTR_BRIGHTNESS), kwargs.get(ATTR_TRANSITION) - ) + await self._async_set_brightness(kwargs.get(ATTR_BRIGHTNESS), transition) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) - async def _async_set_colors(self, colors: dict[ColorComponent, int]) -> None: + async def _async_set_colors( + self, colors: dict[ColorComponent, int], transition: float | None = None + ) -> None: """Set (multiple) defined colors to given value(s).""" # prefer the (new) combined color property # https://github.com/zwave-js/node-zwave-js/pull/1782 @@ -258,21 +279,36 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): CommandClass.SWITCH_COLOR, value_property_key=None, ) + zwave_transition = None + + if self.supports_color_transition: + if transition is not None: + zwave_transition = {TRANSITION_DURATION: f"{int(transition)}s"} + else: + zwave_transition = {TRANSITION_DURATION: "default"} + if combined_color_val and isinstance(combined_color_val.value, dict): colors_dict = {} for color, value in colors.items(): color_name = MULTI_COLOR_MAP[color] colors_dict[color_name] = value # set updated color object - await self.info.node.async_set_value(combined_color_val, colors_dict) + await self.info.node.async_set_value( + combined_color_val, colors_dict, zwave_transition + ) return # fallback to setting the color(s) one by one if multicolor fails # not sure this is needed at all, but just in case for color, value in colors.items(): - await self._async_set_color(color, value) + await self._async_set_color(color, value, zwave_transition) - async def _async_set_color(self, color: ColorComponent, new_value: int) -> None: + async def _async_set_color( + self, + color: ColorComponent, + new_value: int, + transition: dict[str, str] | None = None, + ) -> None: """Set defined color to given value.""" # actually set the new color value target_zwave_value = self.get_zwave_value( @@ -283,10 +319,10 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): if target_zwave_value is None: # guard for unsupported color return - await self.info.node.async_set_value(target_zwave_value, new_value) + await self.info.node.async_set_value(target_zwave_value, new_value, transition) async def _async_set_brightness( - self, brightness: int | None, transition: int | None = None + self, brightness: int | None, transition: float | None = None ) -> None: """Set new brightness to light.""" if brightness is None: @@ -297,40 +333,17 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): zwave_brightness = byte_to_zwave_brightness(brightness) # set transition value before sending new brightness - await self._async_set_transition_duration(transition) - # setting a value requires setting targetValue - await self.info.node.async_set_value(self._target_value, zwave_brightness) - - async def _async_set_transition_duration(self, duration: int | None = None) -> None: - """Set the transition time for the brightness value.""" - if self._dimming_duration is None: - return - # pylint: disable=fixme,unreachable - # TODO: setting duration needs to be fixed upstream - # https://github.com/zwave-js/node-zwave-js/issues/1321 - return - - if duration is None: # type: ignore - # no transition specified by user, use defaults - duration = 7621 # anything over 7620 uses the factory default - else: # pragma: no cover - # transition specified by user - transition = duration - if transition <= 127: - duration = transition + zwave_transition = None + if self.supports_brightness_transition: + if transition is not None: + zwave_transition = {TRANSITION_DURATION: f"{int(transition)}s"} else: - minutes = round(transition / 60) - LOGGER.debug( - "Transition rounded to %d minutes for %s", - minutes, - self.entity_id, - ) - duration = minutes + 128 + zwave_transition = {TRANSITION_DURATION: "default"} - # only send value if it differs from current - # this prevents sending a command for nothing - if self._dimming_duration.value != duration: # pragma: no cover - await self.info.node.async_set_value(self._dimming_duration, duration) + # setting a value requires setting targetValue + await self.info.node.async_set_value( + self._target_brightness, zwave_brightness, zwave_transition + ) @callback def _calculate_color_values(self) -> None: diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 2d9cf06b095..43250c3477d 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -12,6 +12,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, SUPPORT_TRANSITION, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON @@ -62,12 +63,47 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "readable": True, "writeable": True, "label": "Target value", + "valueChangeOptions": ["transitionDuration"], }, } assert args["value"] == 255 client.async_send_command.reset_mock() + # Test turning on with transition + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_TRANSITION: 10}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == 255 + assert args["options"]["transitionDuration"] == "10s" + + client.async_send_command.reset_mock() + # Test brightness update from value updated event event = Event( type="value updated", @@ -133,9 +169,49 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "readable": True, "writeable": True, "label": "Target value", + "valueChangeOptions": ["transitionDuration"], }, } assert args["value"] == 50 + assert args["options"]["transitionDuration"] == "default" + + client.async_send_command.reset_mock() + + # Test turning on with brightness and transition + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, + ATTR_BRIGHTNESS: 129, + ATTR_TRANSITION: 20, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == 50 + assert args["options"]["transitionDuration"] == "20s" client.async_send_command.reset_mock() @@ -256,6 +332,23 @@ async def test_light(hass, client, bulb_6_multi_color, integration): client.async_send_command.reset_mock() + # Test turning on with rgb color and transition + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, + ATTR_RGB_COLOR: (128, 76, 255), + ATTR_TRANSITION: 20, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 6 + args = client.async_send_command.call_args_list[5][0][0] + assert args["options"]["transitionDuration"] == "20s" + client.async_send_command.reset_mock() + # Test turning on with color temp await hass.services.async_call( "light", @@ -377,6 +470,24 @@ async def test_light(hass, client, bulb_6_multi_color, integration): client.async_send_command.reset_mock() + # Test turning on with color temp and transition + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, + ATTR_COLOR_TEMP: 170, + ATTR_TRANSITION: 35, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 6 + args = client.async_send_command.call_args_list[5][0][0] + assert args["options"]["transitionDuration"] == "35s" + + client.async_send_command.reset_mock() + # Test turning off await hass.services.async_call( "light", @@ -403,6 +514,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "readable": True, "writeable": True, "label": "Target value", + "valueChangeOptions": ["transitionDuration"], }, } assert args["value"] == 0 @@ -480,6 +592,7 @@ async def test_rgbw_light(hass, client, zen_31, integration): "readable": True, "writeable": True, "label": "Target value", + "valueChangeOptions": ["transitionDuration"], }, "value": 59, } diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json index 64bfecfb20b..58608131e90 100644 --- a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -75,10 +75,13 @@ "type": "number", "readable": true, "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, - "max": 99, - "label": "Target value" - } + "max": 99 + } }, { "commandClassName": "Multilevel Switch", @@ -339,6 +342,23 @@ "description": "The target value of the Blue color." } }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Target Color", + "valueChangeOptions": [ + "transitionDuration" + ] + } + }, { "commandClassName": "Configuration", "commandClass": 112, diff --git a/tests/fixtures/zwave_js/light_color_null_values_state.json b/tests/fixtures/zwave_js/light_color_null_values_state.json index 46bc9f29b06..f60b61c7a5a 100644 --- a/tests/fixtures/zwave_js/light_color_null_values_state.json +++ b/tests/fixtures/zwave_js/light_color_null_values_state.json @@ -99,6 +99,9 @@ "readable": true, "writeable": true, "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, "max": 99 } diff --git a/tests/fixtures/zwave_js/zen_31_state.json b/tests/fixtures/zwave_js/zen_31_state.json index d322279fbfa..f0c3c1759eb 100644 --- a/tests/fixtures/zwave_js/zen_31_state.json +++ b/tests/fixtures/zwave_js/zen_31_state.json @@ -1430,6 +1430,9 @@ "readable": true, "writeable": true, "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, "max": 99 }, @@ -1982,6 +1985,9 @@ "readable": true, "writeable": true, "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, "max": 99 }, @@ -2100,6 +2106,9 @@ "readable": true, "writeable": true, "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, "max": 99 }, @@ -2218,6 +2227,9 @@ "readable": true, "writeable": true, "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, "max": 99 }, @@ -2336,6 +2348,9 @@ "readable": true, "writeable": true, "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, "max": 99 }, From bbff9622a7e5db997f0bba06d3281166dae73b7a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 9 Jul 2021 19:12:51 +0200 Subject: [PATCH 181/818] Fix Neato parameter for token refresh (#52785) * Fix param * cleanup --- homeassistant/components/neato/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index f61db94332b..28569e0f1d7 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -77,9 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) - - neato_session = api.ConfigEntryAuth(hass, entry, session) + neato_session = api.ConfigEntryAuth(hass, entry, implementation) hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session hub = NeatoHub(hass, Account(neato_session)) From b3472268202ba6b91eb2f713f0a613505c0f9241 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Jul 2021 00:09:24 +0000 Subject: [PATCH 182/818] [ci skip] Translation update --- .../accuweather/translations/de.json | 2 +- .../components/adguard/translations/id.json | 1 + .../components/aemet/translations/id.json | 9 ++++ .../components/agent_dvr/translations/de.json | 2 +- .../components/airnow/translations/de.json | 2 +- .../components/airvisual/translations/de.json | 12 ++--- .../alarmdecoder/translations/de.json | 4 +- .../components/ambee/translations/id.json | 26 ++++++++++ .../components/atag/translations/de.json | 2 +- .../components/august/translations/de.json | 2 +- .../components/blebox/translations/de.json | 6 +-- .../components/bosch_shc/translations/de.json | 6 +-- .../components/bosch_shc/translations/id.json | 19 +++++++ .../components/braviatv/translations/de.json | 6 +-- .../components/broadlink/translations/de.json | 2 +- .../buienradar/translations/id.json | 11 ++++ .../components/cast/translations/de.json | 2 +- .../components/cast/translations/id.json | 11 ++++ .../cloudflare/translations/id.json | 6 +++ .../components/coinbase/translations/id.json | 24 +++++++++ .../coronavirus/translations/de.json | 2 +- .../components/demo/translations/de.json | 2 +- .../components/denonavr/translations/de.json | 2 +- .../components/directv/translations/de.json | 2 +- .../components/doorbird/translations/de.json | 4 +- .../components/dsmr/translations/id.json | 27 +++++++++- .../components/eafm/translations/de.json | 4 +- .../components/elkm1/translations/de.json | 4 +- .../components/emonitor/translations/de.json | 2 +- .../components/enocean/translations/de.json | 4 +- .../enphase_envoy/translations/id.json | 3 +- .../components/ezviz/translations/de.json | 2 +- .../faa_delays/translations/de.json | 2 +- .../components/flume/translations/de.json | 2 +- .../components/flume/translations/id.json | 8 ++- .../forecast_solar/translations/id.json | 11 ++++ .../forked_daapd/translations/de.json | 2 +- .../freedompro/translations/id.json | 11 ++++ .../components/fritz/translations/id.json | 47 +++++++++++++++++ .../garages_amsterdam/translations/de.json | 2 +- .../garages_amsterdam/translations/id.json | 9 ++++ .../components/goalzero/translations/de.json | 2 +- .../components/goalzero/translations/id.json | 4 +- .../components/gogogate2/translations/id.json | 1 + .../google_travel_time/translations/de.json | 4 +- .../google_travel_time/translations/id.json | 1 + .../growatt_server/translations/id.json | 16 ++++++ .../components/guardian/translations/de.json | 2 +- .../components/habitica/translations/de.json | 2 +- .../components/harmony/translations/de.json | 8 +-- .../components/homekit/translations/de.json | 10 ++-- .../homekit_controller/translations/de.json | 6 +-- .../translations/de.json | 6 +-- .../translations/id.json | 1 + .../components/icloud/translations/de.json | 2 +- .../components/insteon/translations/de.json | 34 ++++++------- .../components/ipp/translations/de.json | 2 +- .../components/juicenet/translations/de.json | 2 +- .../components/kodi/translations/de.json | 4 +- .../components/konnected/translations/de.json | 20 ++++---- .../lutron_caseta/translations/de.json | 2 +- .../components/mazda/translations/de.json | 4 +- .../components/melcloud/translations/de.json | 2 +- .../met_eireann/translations/de.json | 2 +- .../meteoclimatic/translations/de.json | 2 +- .../modern_forms/translations/de.json | 2 +- .../components/monoprice/translations/de.json | 2 +- .../components/motioneye/translations/de.json | 2 +- .../components/motioneye/translations/id.json | 23 +++++++++ .../components/myq/translations/de.json | 4 +- .../components/nam/translations/id.json | 21 ++++++++ .../components/netatmo/translations/de.json | 2 +- .../components/nexia/translations/de.json | 2 +- .../components/nexia/translations/id.json | 1 + .../nightscout/translations/de.json | 2 +- .../nmap_tracker/translations/id.json | 17 +++++++ .../components/nuheat/translations/de.json | 4 +- .../components/nws/translations/de.json | 2 +- .../components/omnilogic/translations/id.json | 1 + .../components/onvif/translations/de.json | 16 +++--- .../openweathermap/translations/de.json | 2 +- .../ovo_energy/translations/de.json | 2 +- .../panasonic_viera/translations/de.json | 2 +- .../components/picnic/translations/id.json | 17 +++++++ .../components/plaato/translations/de.json | 4 +- .../components/plex/translations/de.json | 2 +- .../components/plugwise/translations/de.json | 2 +- .../components/powerwall/translations/de.json | 2 +- .../pvpc_hourly_pricing/translations/de.json | 4 +- .../components/rachio/translations/de.json | 2 +- .../rainmachine/translations/id.json | 1 + .../recollect_waste/translations/de.json | 2 +- .../components/risco/translations/de.json | 4 +- .../translations/de.json | 2 +- .../components/roku/translations/de.json | 4 +- .../components/roomba/translations/de.json | 2 +- .../components/roon/translations/de.json | 4 +- .../components/rpi_power/translations/de.json | 2 +- .../components/samsungtv/translations/de.json | 2 +- .../components/samsungtv/translations/id.json | 7 ++- .../components/select/translations/id.json | 14 ++++++ .../components/sense/translations/de.json | 2 +- .../components/shelly/translations/de.json | 2 +- .../shopping_list/translations/de.json | 2 +- .../components/sia/translations/de.json | 6 +-- .../simplisafe/translations/de.json | 2 +- .../components/sma/translations/de.json | 2 +- .../smartthings/translations/de.json | 4 +- .../components/smarttub/translations/id.json | 3 ++ .../components/soma/translations/de.json | 2 +- .../components/songpal/translations/de.json | 2 +- .../components/spotify/translations/de.json | 2 +- .../components/subaru/translations/de.json | 2 +- .../components/syncthing/translations/id.json | 21 ++++++++ .../synology_dsm/translations/de.json | 4 +- .../system_bridge/translations/id.json | 32 ++++++++++++ .../components/tibber/translations/de.json | 2 +- .../components/toon/translations/de.json | 4 +- .../totalconnect/translations/de.json | 2 +- .../components/tuya/translations/de.json | 6 +-- .../components/unifi/translations/de.json | 10 ++-- .../components/upnp/translations/de.json | 2 +- .../components/upnp/translations/id.json | 10 ++++ .../components/vilfo/translations/de.json | 2 +- .../components/vizio/translations/de.json | 6 +-- .../components/wallbox/translations/id.json | 19 +++++++ .../waze_travel_time/translations/de.json | 4 +- .../components/wilight/translations/de.json | 2 +- .../xiaomi_miio/translations/de.json | 6 +-- .../xiaomi_miio/translations/id.json | 45 +++++++++++++++++ .../yamaha_musiccast/translations/id.json | 18 +++++++ .../components/yeelight/translations/de.json | 4 +- .../components/yeelight/translations/id.json | 4 ++ .../components/zha/translations/de.json | 8 +-- .../components/zha/translations/id.json | 14 ++++++ .../components/zwave_js/translations/id.json | 50 +++++++++++++++++++ 136 files changed, 745 insertions(+), 191 deletions(-) create mode 100644 homeassistant/components/ambee/translations/id.json create mode 100644 homeassistant/components/bosch_shc/translations/id.json create mode 100644 homeassistant/components/buienradar/translations/id.json create mode 100644 homeassistant/components/coinbase/translations/id.json create mode 100644 homeassistant/components/forecast_solar/translations/id.json create mode 100644 homeassistant/components/freedompro/translations/id.json create mode 100644 homeassistant/components/fritz/translations/id.json create mode 100644 homeassistant/components/garages_amsterdam/translations/id.json create mode 100644 homeassistant/components/growatt_server/translations/id.json create mode 100644 homeassistant/components/motioneye/translations/id.json create mode 100644 homeassistant/components/nam/translations/id.json create mode 100644 homeassistant/components/nmap_tracker/translations/id.json create mode 100644 homeassistant/components/picnic/translations/id.json create mode 100644 homeassistant/components/select/translations/id.json create mode 100644 homeassistant/components/syncthing/translations/id.json create mode 100644 homeassistant/components/system_bridge/translations/id.json create mode 100644 homeassistant/components/wallbox/translations/id.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/id.json diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index a9b23bacf6c..17eb0ee31fc 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", - "requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Sie m\u00fcssen warten oder den API-Schl\u00fcssel \u00e4ndern." + "requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Du musst warten oder den API-Schl\u00fcssel \u00e4ndern." }, "step": { "user": { diff --git a/homeassistant/components/adguard/translations/id.json b/homeassistant/components/adguard/translations/id.json index d787fd5620d..d3334997f59 100644 --- a/homeassistant/components/adguard/translations/id.json +++ b/homeassistant/components/adguard/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Layanan sudah dikonfigurasi", "existing_instance_updated": "Memperbarui konfigurasi yang ada." }, "error": { diff --git a/homeassistant/components/aemet/translations/id.json b/homeassistant/components/aemet/translations/id.json index fa678cbbbe0..e3a602a9a7c 100644 --- a/homeassistant/components/aemet/translations/id.json +++ b/homeassistant/components/aemet/translations/id.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Kumpulkan data dari stasiun cuaca AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json index 10a8307ada1..a8c31dd0ca8 100644 --- a/homeassistant/components/agent_dvr/translations/de.json +++ b/homeassistant/components/agent_dvr/translations/de.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Port" }, - "title": "Richten Sie den Agent DVR ein" + "title": "Richte den Agent DVR ein" } } } diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json index 646369b6b61..adf9ddf85a3 100644 --- a/homeassistant/components/airnow/translations/de.json +++ b/homeassistant/components/airnow/translations/de.json @@ -17,7 +17,7 @@ "longitude": "L\u00e4ngengrad", "radius": "Stationsradius (Meilen; optional)" }, - "description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.", + "description": "Richte die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://docs.airnowapi.org/account/request/.", "title": "AirNow" } } diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 588d69f96fe..d5b9fd915d3 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -35,8 +35,8 @@ "ip_address": "Host", "password": "Passwort" }, - "description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", - "title": "Konfigurieren Sie einen AirVisual Node/Pro" + "description": "\u00dcberwache eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", + "title": "Konfiguriere einen AirVisual Node/Pro" }, "reauth_confirm": { "data": { @@ -45,8 +45,8 @@ "title": "AirVisual erneut authentifizieren" }, "user": { - "description": "W\u00e4hlen Sie aus, welche Art von AirVisual-Daten Sie \u00fcberwachen m\u00f6chten.", - "title": "Konfigurieren Sie AirVisual" + "description": "W\u00e4hle aus, welche Art von AirVisual-Daten du \u00fcberwachen m\u00f6chtest.", + "title": "Konfiguriere AirVisual" } } }, @@ -54,9 +54,9 @@ "step": { "init": { "data": { - "show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" + "show_on_map": "Zeige die \u00fcberwachte Geografie auf der Karte an" }, - "title": "Konfigurieren Sie AirVisual" + "title": "Konfiguriere AirVisual" } } } diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index 4936cce4dfd..f324772c909 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -23,7 +23,7 @@ "data": { "protocol": "Protokoll" }, - "title": "W\u00e4hlen Sie das AlarmDecoder-Protokoll" + "title": "W\u00e4hle das AlarmDecoder-Protokoll" } } }, @@ -59,7 +59,7 @@ "zone_rfid": "RF Serial", "zone_type": "Zonentyp" }, - "description": "Geben Sie Details f\u00fcr Zone {zone_number} ein. Um Zone {zone_number} zu l\u00f6schen, lassen Sie Zonenname leer.", + "description": "Gib Details f\u00fcr Zone {zone_number} ein. Um Zone {zone_number} zu l\u00f6schen, lass den Zonennamen leer.", "title": "AlarmDecoder konfigurieren" }, "zone_select": { diff --git a/homeassistant/components/ambee/translations/id.json b/homeassistant/components/ambee/translations/id.json new file mode 100644 index 00000000000..ecf627579fe --- /dev/null +++ b/homeassistant/components/ambee/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + } + }, + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 72e8c69cc26..8d91f5b62fa 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Port" }, - "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her" + "title": "Verbinden mit dem Ger\u00e4t" } } } diff --git a/homeassistant/components/august/translations/de.json b/homeassistant/components/august/translations/de.json index d2e08a5377c..7f52aa083e7 100644 --- a/homeassistant/components/august/translations/de.json +++ b/homeassistant/components/august/translations/de.json @@ -30,7 +30,7 @@ "data": { "code": "Verifizierungs-Code" }, - "description": "Bitte \u00fcberpr\u00fcfen Sie Ihre {login_method} ({username}) und geben Sie den Best\u00e4tigungscode ein", + "description": "Bitte \u00fcberpr\u00fcfe deine {login_method} ({username}) und gib den Best\u00e4tigungscode ein", "title": "Zwei-Faktor-Authentifizierung" } } diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index 508e4b66ee6..bb1f9e9c443 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler", - "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst." + "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisiere es zuerst." }, "flow_title": "{name} ( {host} )", "step": { @@ -16,8 +16,8 @@ "host": "IP Adresse", "port": "Port" }, - "description": "Richten Sie Ihre BleBox f\u00fcr die Integration mit dem Home Assistant ein.", - "title": "Richten Sie Ihr BleBox-Ger\u00e4t ein" + "description": "Richte deine BleBox f\u00fcr die Integration mit dem Home Assistant ein.", + "title": "Richte dein BleBox-Ger\u00e4t ein" } } } diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index 110e986e106..c99af84b8b5 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "pairing_failed": "Pairing fehlgeschlagen; bitte pr\u00fcfen Sie, ob sich der Bosch Smart Home Controller im Pairing-Modus befindet (LED blinkt) und ob Ihr Passwort korrekt ist.", + "pairing_failed": "Pairing fehlgeschlagen; bitte pr\u00fcfe, ob sich der Bosch Smart Home Controller im Pairing-Modus befindet (LED blinkt) und ob dein Passwort korrekt ist.", "session_error": "Sitzungsfehler: API gab Non-OK-Ergebnis zur\u00fcck.", "unknown": "Unerwarteter Fehler" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Bitte dr\u00fccken Sie die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nSind Sie bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" + "description": "Bitte dr\u00fccke die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" }, "credentials": { "data": { @@ -29,7 +29,7 @@ "data": { "host": "Host" }, - "description": "Richten Sie Ihren Bosch Smart Home Controller ein, um die \u00dcberwachung und Steuerung mit Home Assistant zu erm\u00f6glichen.", + "description": "Richte deinen Bosch Smart Home Controller ein, um die \u00dcberwachung und Steuerung mit Home Assistant zu erm\u00f6glichen.", "title": "SHC Authentifizierungsparameter" } } diff --git a/homeassistant/components/bosch_shc/translations/id.json b/homeassistant/components/bosch_shc/translations/id.json new file mode 100644 index 00000000000..c2167eb0f20 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "reauth_confirm": { + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 7dfff8a1b44..dd1eadb630a 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -14,14 +14,14 @@ "data": { "pin": "PIN-Code" }, - "description": "Geben Sie den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, m\u00fcssen Sie die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehen Sie daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", - "title": "Autorisieren Sie Sony Bravia TV" + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", + "title": "Autorisiere Sony Bravia TV" }, "user": { "data": { "host": "Host" }, - "description": "Richten Sie die Sony Bravia TV-Integration ein. Wenn Sie Probleme mit der Konfiguration haben, gehen Sie zu: https://www.home-assistant.io/integrations/braviatv \n\n Stellen Sie sicher, dass Ihr Fernseher eingeschaltet ist.", + "description": "Richte die Sony Bravia TV-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/braviatv \n\nStelle sicher, dass dein Fernseher eingeschaltet ist.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index d81c131bf5d..e0e819e2140 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -32,7 +32,7 @@ "data": { "unlock": "Ja mach das." }, - "description": "{name} ({model} unter {host}) ist gesperrt. Dies kann zu Authentifizierungsproblemen im Home Assistant f\u00fchren. M\u00f6chten Sie es entsperren?", + "description": "{name} ({model} unter {host}) ist gesperrt. Dies kann zu Authentifizierungsproblemen im Home Assistant f\u00fchren. M\u00f6chtest du es entsperren?", "title": "Entsperren des Ger\u00e4ts (optional)" }, "user": { diff --git a/homeassistant/components/buienradar/translations/id.json b/homeassistant/components/buienradar/translations/id.json new file mode 100644 index 00000000000..194ecb51c12 --- /dev/null +++ b/homeassistant/components/buienradar/translations/id.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Bujur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 98bddf0d7d0..3e03e6a3b73 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -29,7 +29,7 @@ "ignore_cec": "CEC ignorieren", "uuid": "Zul\u00e4ssige UUIDs" }, - "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn Sie nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chten.\nCEC ignorieren - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.", + "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn du nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chtest.\nCEC ignorieren - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.", "title": "Erweiterte Google Cast-Konfiguration" }, "basic_options": { diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index bd7e0c936b1..b2c8d515548 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -22,6 +22,17 @@ "options": { "error": { "invalid_known_hosts": "Host yang diketahui harus berupa daftar host yang dipisahkan koma." + }, + "step": { + "advanced_options": { + "title": "Konfigurasi Google Cast tingkat lanjut" + }, + "basic_options": { + "data": { + "known_hosts": "Host yang dikenal" + }, + "title": "Konfigurasi Google Cast" + } } } } \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/id.json b/homeassistant/components/cloudflare/translations/id.json index 98286398ea8..c7878017de3 100644 --- a/homeassistant/components/cloudflare/translations/id.json +++ b/homeassistant/components/cloudflare/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Autentikasi ulang berhasil", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -11,6 +12,11 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "Token API" + } + }, "records": { "data": { "records": "Catatan" diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json new file mode 100644 index 00000000000..e0d93019507 --- /dev/null +++ b/homeassistant/components/coinbase/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API" + } + } + } + }, + "options": { + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/de.json b/homeassistant/components/coronavirus/translations/de.json index 45eaff64200..25a1cf44ca5 100644 --- a/homeassistant/components/coronavirus/translations/de.json +++ b/homeassistant/components/coronavirus/translations/de.json @@ -9,7 +9,7 @@ "data": { "country": "Land" }, - "title": "W\u00e4hlen Sie ein Land aus, das \u00fcberwacht werden soll" + "title": "W\u00e4hle ein Land aus, das \u00fcberwacht werden soll" } } } diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index 8d737e5e4c9..74178521138 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -17,7 +17,7 @@ "options_2": { "data": { "multi": "Mehrfachauswahl", - "select": "W\u00e4hlen Sie eine Option", + "select": "W\u00e4hle eine Option", "string": "String-Wert" } } diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index f40c665489c..300131280ac 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut.", + "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuche es noch einmal. Trenne ggf. Strom- und Ethernetkabel und verbinde diese erneut.", "not_denonavr_manufacturer": "Kein Denon AVR-Netzwerkempf\u00e4nger, entdeckter Hersteller stimmte nicht \u00fcberein", "not_denonavr_missing": "Kein Denon AVR-Netzwerk-Receiver, Erkennungsinformationen nicht vollst\u00e4ndig" }, diff --git a/homeassistant/components/directv/translations/de.json b/homeassistant/components/directv/translations/de.json index de5cc512940..c4a6ed1791e 100644 --- a/homeassistant/components/directv/translations/de.json +++ b/homeassistant/components/directv/translations/de.json @@ -14,7 +14,7 @@ "one": "eins", "other": "andere" }, - "description": "M\u00f6chten Sie {name} einrichten?" + "description": "M\u00f6chtest du {name} einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index 640f13a73c6..b558bd0b222 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -19,7 +19,7 @@ "password": "Passwort", "username": "Benutzername" }, - "title": "Stellen Sie eine Verbindung zu DoorBird her" + "title": "Stelle eine Verbindung zu DoorBird her" } } }, @@ -29,7 +29,7 @@ "data": { "events": "Durch Kommas getrennte Liste von Ereignissen." }, - "description": "F\u00fcgen Sie f\u00fcr jedes Ereignis, das Sie verfolgen m\u00f6chten, einen durch Kommas getrennten Ereignisnamen hinzu. Nachdem Sie sie hier eingegeben haben, verwenden Sie die DoorBird-App, um sie einem bestimmten Ereignis zuzuweisen. Weitere Informationen finden Sie in der Dokumentation unter https://www.home-assistant.io/integrations/doorbird/#events. Beispiel: jemand_hat_den_knopf_gedr\u00fcckt, bewegung" + "description": "F\u00fcge f\u00fcr jedes Ereignis, das du verfolgen m\u00f6chtest, einen durch Kommas getrennten Ereignisnamen hinzu. Nachdem du sie hier eingegeben hast, verwende die DoorBird-App, um sie einem bestimmten Ereignis zuzuweisen. Weitere Informationen findest du in der Dokumentation unter https://www.home-assistant.io/integrations/doorbird/#events. Beispiel: jemand_hat_den_knopf_gedr\u00fcckt, bewegung" } } } diff --git a/homeassistant/components/dsmr/translations/id.json b/homeassistant/components/dsmr/translations/id.json index fd8299d61ed..2e56dd3b0a6 100644 --- a/homeassistant/components/dsmr/translations/id.json +++ b/homeassistant/components/dsmr/translations/id.json @@ -1,7 +1,32 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "step": { + "setup_network": { + "data": { + "host": "Host", + "port": "Port" + } + }, + "setup_serial_manual_path": { + "data": { + "port": "Jalur Perangkat USB" + }, + "title": "Jalur" + }, + "user": { + "data": { + "type": "Jenis koneksi" + }, + "title": "Pilih jenis koneksi" + } } }, "options": { diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json index 46185acc11b..c82a21b1c3e 100644 --- a/homeassistant/components/eafm/translations/de.json +++ b/homeassistant/components/eafm/translations/de.json @@ -9,8 +9,8 @@ "data": { "station": "Station" }, - "description": "W\u00e4hlen Sie die Station aus, die Sie \u00fcberwachen m\u00f6chten", - "title": "Verfolgen Sie eine Hochwasser\u00fcberwachungsstation" + "description": "W\u00e4hle die Station aus, die du \u00fcberwachen m\u00f6chtest", + "title": "Verfolge eine Hochwasser\u00fcberwachungsstation" } } } diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json index 8157a061d82..137f781fd05 100644 --- a/homeassistant/components/elkm1/translations/de.json +++ b/homeassistant/components/elkm1/translations/de.json @@ -14,13 +14,13 @@ "data": { "address": "Die IP-Adresse, die Domain oder der serielle Port bei einer seriellen Verbindung.", "password": "Passwort", - "prefix": "Ein eindeutiges Pr\u00e4fix (leer lassen, wenn Sie nur einen ElkM1 haben).", + "prefix": "Ein eindeutiges Pr\u00e4fix (leer lassen, wenn du nur einen ElkM1 hast).", "protocol": "Protokoll", "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.", "username": "Benutzername" }, "description": "Die Adresszeichenfolge muss in der Form 'adresse[:port]' f\u00fcr 'sicher' und 'nicht sicher' vorliegen. Beispiel: '192.168.1.1'. Der Port ist optional und standardm\u00e4\u00dfig 2101 f\u00fcr \"nicht sicher\" und 2601 f\u00fcr \"sicher\". F\u00fcr das serielle Protokoll muss die Adresse die Form 'tty[:baud]' haben. Beispiel: '/dev/ttyS1'. Der Baudrate ist optional und standardm\u00e4\u00dfig 115200.", - "title": "Stellen Sie eine Verbindung zur Elk-M1-Steuerung her" + "title": "Stelle eine Verbindung zur Elk-M1-Steuerung her" } } } diff --git a/homeassistant/components/emonitor/translations/de.json b/homeassistant/components/emonitor/translations/de.json index c36f7a5ae77..b1b10756c31 100644 --- a/homeassistant/components/emonitor/translations/de.json +++ b/homeassistant/components/emonitor/translations/de.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "description": "M\u00f6chtest du {name} ({host}) einrichten?", "title": "Einrichtung SiteSage Emonitor" }, "user": { diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json index a8e4e2c7f84..fe7467fbf09 100644 --- a/homeassistant/components/enocean/translations/de.json +++ b/homeassistant/components/enocean/translations/de.json @@ -12,13 +12,13 @@ "data": { "path": "USB-Dongle-Pfad" }, - "title": "W\u00e4hlen Sie den Pfad zu Ihrem ENOcean-Dongle" + "title": "W\u00e4hle den Pfad zu deinem ENOcean-Dongle" }, "manual": { "data": { "path": "USB-Dongle-Pfad" }, - "title": "Geben Sie den Pfad zu Ihrem ENOcean-Dongle ein" + "title": "Gib den Pfad zu deinem ENOcean-Dongle ein" } } } diff --git a/homeassistant/components/enphase_envoy/translations/id.json b/homeassistant/components/enphase_envoy/translations/id.json index 74e3e8a66c7..ba3f8dd8cc6 100644 --- a/homeassistant/components/enphase_envoy/translations/id.json +++ b/homeassistant/components/enphase_envoy/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json index ab860d44201..92faeff2b81 100644 --- a/homeassistant/components/ezviz/translations/de.json +++ b/homeassistant/components/ezviz/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Konto wurde bereits konfiguriert", - "ezviz_cloud_account_missing": "Ezviz-Cloud-Konto fehlt. Bitte konfigurieren Sie das Ezviz-Cloud-Konto neu", + "ezviz_cloud_account_missing": "Ezviz-Cloud-Konto fehlt. Bitte konfiguriere das Ezviz-Cloud-Konto neu", "unknown": "Unerwarteter Fehler" }, "error": { diff --git a/homeassistant/components/faa_delays/translations/de.json b/homeassistant/components/faa_delays/translations/de.json index 9519c7d4470..b10619dc23d 100644 --- a/homeassistant/components/faa_delays/translations/de.json +++ b/homeassistant/components/faa_delays/translations/de.json @@ -13,7 +13,7 @@ "data": { "id": "Flughafen" }, - "description": "Geben Sie einen US-Flughafencode im IATA-Format ein", + "description": "Gib einen US-Flughafencode im IATA-Format ein", "title": "FAA Delays" } } diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index 2d1a67d9a74..f40fb92edcd 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -15,7 +15,7 @@ "password": "Passwort" }, "description": "Das Passwort f\u00fcr {username} ist nicht mehr g\u00fcltig.", - "title": "Authentifizieren Sie Ihr Flume-Konto erneut" + "title": "Authentifiziere dein Flume-Konto erneut" }, "user": { "data": { diff --git a/homeassistant/components/flume/translations/id.json b/homeassistant/components/flume/translations/id.json index 333afb167e6..f72e27ece8d 100644 --- a/homeassistant/components/flume/translations/id.json +++ b/homeassistant/components/flume/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,11 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + } + }, "user": { "data": { "client_id": "ID Klien", diff --git a/homeassistant/components/forecast_solar/translations/id.json b/homeassistant/components/forecast_solar/translations/id.json new file mode 100644 index 00000000000..b0a5ddcdc7e --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/id.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Lintang" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 2047414b168..a475becc605 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -5,7 +5,7 @@ "not_forked_daapd": "Das Ger\u00e4t ist kein Forked-Daapd-Server." }, "error": { - "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Ihre forked-daapd-Netzwerkberechtigungen.", + "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfe deine forked-daapd-Netzwerkberechtigungen.", "unknown_error": "Unbekannter Fehler", "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", diff --git a/homeassistant/components/freedompro/translations/id.json b/homeassistant/components/freedompro/translations/id.json new file mode 100644 index 00000000000..82523dc65d1 --- /dev/null +++ b/homeassistant/components/freedompro/translations/id.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kunci API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json new file mode 100644 index 00000000000..1a3140da624 --- /dev/null +++ b/homeassistant/components/fritz/translations/id.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "connection_error": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "start_config": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/de.json b/homeassistant/components/garages_amsterdam/translations/de.json index aa13e22eabb..8a35e2f2e26 100644 --- a/homeassistant/components/garages_amsterdam/translations/de.json +++ b/homeassistant/components/garages_amsterdam/translations/de.json @@ -10,7 +10,7 @@ "data": { "garage_name": "Name der Garage" }, - "title": "W\u00e4hlen Sie eine Garage zur \u00dcberwachung aus" + "title": "W\u00e4hle eine Garage zur \u00dcberwachung aus" } } }, diff --git a/homeassistant/components/garages_amsterdam/translations/id.json b/homeassistant/components/garages_amsterdam/translations/id.json new file mode 100644 index 00000000000..37a312250a1 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/id.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 6f6eb052589..3049a4f77ef 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -12,7 +12,7 @@ }, "step": { "confirm_discovery": { - "description": "Eine DHCP-Reservierung auf Ihrem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlagen Sie im Benutzerhandbuch Ihres Routers nach.", + "description": "Eine DHCP-Reservierung auf Ihrem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage im Benutzerhandbuch deines Routers nach.", "title": "Goal Zero Yeti" }, "user": { diff --git a/homeassistant/components/goalzero/translations/id.json b/homeassistant/components/goalzero/translations/id.json index 63fddf13a8e..5bab8fa03a2 100644 --- a/homeassistant/components/goalzero/translations/id.json +++ b/homeassistant/components/goalzero/translations/id.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "invalid_host": "Nama host atau alamat IP tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/gogogate2/translations/id.json b/homeassistant/components/gogogate2/translations/id.json index 9de61641d41..89d25d74a48 100644 --- a/homeassistant/components/gogogate2/translations/id.json +++ b/homeassistant/components/gogogate2/translations/id.json @@ -7,6 +7,7 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json index 4e89ad7da1a..701935f53fe 100644 --- a/homeassistant/components/google_travel_time/translations/de.json +++ b/homeassistant/components/google_travel_time/translations/de.json @@ -14,7 +14,7 @@ "name": "Name", "origin": "Startort" }, - "description": "Bei der Angabe von Ursprung und Ziel k\u00f6nnen Sie einen oder mehrere durch das Pipe-Zeichen getrennte Orte in Form einer Adresse, L\u00e4ngen- / Breitengradkoordinaten oder einer Google-Orts-ID angeben. Wenn Sie den Standort mithilfe einer Google-Orts-ID angeben, muss der ID \"place_id:\" vorangestellt werden." + "description": "Bei der Angabe von Ursprung und Ziel kannst du einen oder mehrere durch das Pipe-Zeichen getrennte Orte in Form einer Adresse, L\u00e4ngen- / Breitengradkoordinaten oder einer Google-Orts-ID angeben. Wenn du den Standort mithilfe einer Google-Orts-ID angibst, muss der ID \"place_id:\" vorangestellt werden." } } }, @@ -31,7 +31,7 @@ "transit_routing_preference": "Transit-Routing-Einstellungen", "units": "Einheiten" }, - "description": "Sie k\u00f6nnen optional entweder eine Abfahrtszeit oder eine Ankunftszeit angeben. Wenn Sie eine Abfahrtszeit angeben, k\u00f6nnen Sie \"Now\", einen Unix-Zeitstempel oder eine 24-Stunden-Zeichenkette wie \"08:00:00\" eingeben. Wenn Sie eine Ankunftszeit angeben, k\u00f6nnen Sie einen Unix-Zeitstempel oder eine 24-Stunden-Zeichenkette wie \"08:00:00\" verwenden." + "description": "Du kannst optional entweder eine Abfahrtszeit oder eine Ankunftszeit angeben. Wenn du eine Abfahrtszeit angibst, kannst du \"Now\", einen Unix-Zeitstempel oder eine 24-Stunden-Zeichenkette wie \"08:00:00\" eingeben. Wenn du eine Ankunftszeit angibst, kannst du einen Unix-Zeitstempel oder eine 24-Stunden-Zeichenkette wie \"08:00:00\" verwenden." } } }, diff --git a/homeassistant/components/google_travel_time/translations/id.json b/homeassistant/components/google_travel_time/translations/id.json index 3973d673f8e..16b60148aa9 100644 --- a/homeassistant/components/google_travel_time/translations/id.json +++ b/homeassistant/components/google_travel_time/translations/id.json @@ -11,6 +11,7 @@ "data": { "api_key": "Kunci API", "destination": "Tujuan", + "name": "Nama", "origin": "Asal" }, "description": "Saat menentukan asal dan tujuan, Anda dapat menyediakan satu atau beberapa lokasi yang dipisahkan oleh karakter pipe, dalam bentuk alamat, koordinat lintang/bujur, atau ID tempat Google. Saat menentukan lokasi menggunakan ID tempat Google, ID harus diawali dengan \"place_id:'." diff --git a/homeassistant/components/growatt_server/translations/id.json b/homeassistant/components/growatt_server/translations/id.json new file mode 100644 index 00000000000..789d4e1732b --- /dev/null +++ b/homeassistant/components/growatt_server/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "name": "Nama", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index fc3ca8fee06..63949b22de6 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -7,7 +7,7 @@ }, "step": { "discovery_confirm": { - "description": "M\u00f6chten Sie dieses Guardian-Ger\u00e4t einrichten?" + "description": "M\u00f6chtest du dieses Guardian-Ger\u00e4t einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json index ad4f3d2aff8..694bbdd65d4 100644 --- a/homeassistant/components/habitica/translations/de.json +++ b/homeassistant/components/habitica/translations/de.json @@ -12,7 +12,7 @@ "name": "Override f\u00fcr den Benutzernamen von Habitica. Wird f\u00fcr Serviceaufrufe verwendet", "url": "URL" }, - "description": "Verbinden Sie Ihr Habitica-Profil, um die \u00dcberwachung des Profils und der Aufgaben Ihres Benutzers zu erm\u00f6glichen. Beachten Sie, dass api_id und api_key von https://habitica.com/user/settings/api bezogen werden m\u00fcssen." + "description": "Verbinde dein Habitica-Profil, um die \u00dcberwachung des Profils und der Aufgaben deines Benutzers zu erm\u00f6glichen. Beachte, dass api_id und api_key von https://habitica.com/user/settings/api bezogen werden m\u00fcssen." } } }, diff --git a/homeassistant/components/harmony/translations/de.json b/homeassistant/components/harmony/translations/de.json index 5083ccd848f..24cdc51cd65 100644 --- a/homeassistant/components/harmony/translations/de.json +++ b/homeassistant/components/harmony/translations/de.json @@ -10,15 +10,15 @@ "flow_title": "{name}", "step": { "link": { - "description": "M\u00f6chten Sie {name} ({host}) einrichten?", - "title": "Richten Sie den Logitech Harmony Hub ein" + "description": "M\u00f6chtest du {name} ({host}) einrichten?", + "title": "Richte den Logitech Harmony Hub ein" }, "user": { "data": { "host": "Host", "name": "Hub-Name" }, - "title": "Richten Sie den Logitech Harmony Hub ein" + "title": "Richte den Logitech Harmony Hub ein" } } }, @@ -29,7 +29,7 @@ "activity": "Die Standardaktivit\u00e4t, die ausgef\u00fchrt werden soll, wenn keine angegeben ist.", "delay_secs": "Die Verz\u00f6gerung zwischen dem Senden von Befehlen." }, - "description": "Passen Sie die Harmony Hub-Optionen an" + "description": "Passe die Harmony Hub-Optionen an" } } } diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index e115c932ac4..759a33fca91 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -5,14 +5,14 @@ }, "step": { "pairing": { - "description": "Um die Kopplung abzuschlie\u00dfen, folgen Sie den Anweisungen in \"Benachrichtigungen\" unter \"HomeKit-Kopplung\".", + "description": "Um die Kopplung abzuschlie\u00dfen, folge den Anweisungen in \"Benachrichtigungen\" unter \"HomeKit-Kopplung\".", "title": "HomeKit verbinden" }, "user": { "data": { "include_domains": "Einzubeziehende Domains" }, - "description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.", + "description": "W\u00e4hle die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.", "title": "W\u00e4hle die zu einzubeziehenden Dom\u00e4nen aus." } } @@ -31,14 +31,14 @@ "camera_copy": "Kameras, die native H.264-Streams unterst\u00fctzen" }, "description": "Pr\u00fcfe alle Kameras, die native H.264-Streams unterst\u00fctzen. Wenn die Kamera keinen H.264-Stream ausgibt, transkodiert das System das Video in H.264 f\u00fcr HomeKit. Die Transkodierung erfordert eine leistungsstarke CPU und wird wahrscheinlich nicht auf Einplatinencomputern funktionieren.", - "title": "W\u00e4hlen Sie den Kamera-Video-Codec." + "title": "W\u00e4hle den Kamera-Video-Codec." }, "include_exclude": { "data": { "entities": "Entit\u00e4ten", "mode": "Modus" }, - "description": "W\u00e4hlen Sie die einzubeziehenden Entit\u00e4ten aus. Im Zubeh\u00f6rmodus wird nur eine einzelne Entit\u00e4t eingeschlossen. Im Bridge-Include-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt sind. Im Bridge-Exclude-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, au\u00dfer den ausgeschlossenen Entit\u00e4ten. F\u00fcr eine optimale Leistung wird f\u00fcr jeden TV-Media-Player, jede aktivit\u00e4tsbasierte Fernbedienung, jedes Schloss und jede Kamera ein separates HomeKit-Zubeh\u00f6r erstellt.", + "description": "W\u00e4hle die einzubeziehenden Entit\u00e4ten aus. Im Zubeh\u00f6rmodus wird nur eine einzelne Entit\u00e4t eingeschlossen. Im Bridge-Include-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt sind. Im Bridge-Exclude-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, au\u00dfer den ausgeschlossenen Entit\u00e4ten. F\u00fcr eine optimale Leistung wird f\u00fcr jeden TV-Media-Player, jede aktivit\u00e4tsbasierte Fernbedienung, jedes Schloss und jede Kamera ein separates HomeKit-Zubeh\u00f6r erstellt.", "title": "W\u00e4hle die Entit\u00e4ten aus, die aufgenommen werden sollen" }, "init": { @@ -46,7 +46,7 @@ "include_domains": "Einzubeziehende Domains", "mode": "Modus" }, - "description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm k\u00f6nnen Sie ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.", + "description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm kannst du ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.", "title": "W\u00e4hle die zu einzubeziehenden Dom\u00e4nen aus." }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 7df1d0fc1a7..1fac267a0ad 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -21,11 +21,11 @@ "flow_title": "{name}", "step": { "busy_error": { - "description": "Brechen Sie das Pairing auf allen Controllern ab oder versuchen Sie, das Ger\u00e4t neu zu starten, und fahren Sie dann fort, das Pairing fortzusetzen.", + "description": "Breche das Pairing auf allen Controllern ab oder versuche, das Ger\u00e4t neu zu starten, und fahre dann fort, das Pairing fortzusetzen.", "title": "Das Ger\u00e4t wird bereits mit einem anderen Controller gekoppelt" }, "max_tries_error": { - "description": "Das Ger\u00e4t hat mehr als 100 erfolglose Authentifizierungsversuche erhalten. Versuchen Sie, das Ger\u00e4t neu zu starten, und fahren Sie dann fort, die Kopplung fortzusetzen.", + "description": "Das Ger\u00e4t hat mehr als 100 erfolglose Authentifizierungsversuche erhalten. Versuche, das Ger\u00e4t neu zu starten, und fahre dann fort, die Kopplung fortzusetzen.", "title": "Maximale Authentifizierungsversuche \u00fcberschritten" }, "pair": { @@ -37,7 +37,7 @@ "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "protocol_error": { - "description": "Das Ger\u00e4t befindet sich m\u00f6glicherweise nicht im Pairing-Modus und erfordert einen physischen oder virtuellen Tastendruck. Stellen Sie sicher, dass sich das Ger\u00e4t im Pairing-Modus befindet, oder versuchen Sie, das Ger\u00e4t neu zu starten und fahren Sie dann das Pairing fort.", + "description": "Das Ger\u00e4t befindet sich m\u00f6glicherweise nicht im Pairing-Modus und erfordert einen physischen oder virtuellen Tastendruck. Stelle sicher, dass sich das Ger\u00e4t im Pairing-Modus befindet, oder versuche, das Ger\u00e4t neu zu starten und fahre dann das Pairing fort.", "title": "Fehler bei der Kommunikation mit dem Zubeh\u00f6r" }, "user": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/de.json b/homeassistant/components/hunterdouglas_powerview/translations/de.json index db0fa18cc29..591e4ee8924 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/de.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/de.json @@ -10,14 +10,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "M\u00f6chten Sie {name} ({host}) einrichten?", - "title": "Stellen Sie eine Verbindung zum PowerView Hub her" + "description": "M\u00f6chtest du {name} ({host}) einrichten?", + "title": "Stelle eine Verbindung zum PowerView Hub her" }, "user": { "data": { "host": "IP-Adresse" }, - "title": "Stellen Sie eine Verbindung zum PowerView Hub her" + "title": "Stelle eine Verbindung zum PowerView Hub her" } } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/id.json b/homeassistant/components/hunterdouglas_powerview/translations/id.json index 2d21f87bf67..273e2badd53 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/id.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/id.json @@ -7,6 +7,7 @@ "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Ingin menyiapkan {name} ({host})?", diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 7baf9dc3917..431b9f35da2 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -15,7 +15,7 @@ "data": { "password": "Passwort" }, - "description": "Ihr zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisieren Sie Ihr Passwort, um diese Integration weiterhin zu verwenden.", + "description": "Dein zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisiere dein Passwort, um diese Integration weiterhin zu verwenden.", "title": "Integration erneut authentifizieren" }, "trusted_device": { diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index 84b53c4aa15..0866f53481f 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "select_single": "W\u00e4hlen Sie eine Option aus." + "select_single": "W\u00e4hle eine Option aus." }, "step": { "hubv1": { @@ -14,7 +14,7 @@ "host": "IP-Adresse", "port": "Port" }, - "description": "Konfigurieren Sie den Insteon Hub Version 1 (vor 2014).", + "description": "Konfiguriere den Insteon Hub Version 1 (vor 2014).", "title": "Insteon Hub Version 1" }, "hubv2": { @@ -24,21 +24,21 @@ "port": "Port", "username": "Benutzername" }, - "description": "Konfigurieren Sie den Insteon Hub Version 2.", + "description": "Konfiguriere den Insteon Hub Version 2.", "title": "Insteon Hub Version 2" }, "plm": { "data": { "device": "USB-Ger\u00e4te-Pfad" }, - "description": "Konfigurieren Sie das Insteon PowerLink Modem (PLM).", + "description": "Konfiguriere das Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" }, "user": { "data": { "modem_type": "Modemtyp." }, - "description": "W\u00e4hlen Sie den Insteon-Modemtyp aus.", + "description": "W\u00e4hle den Insteon-Modemtyp aus.", "title": "Insteon" } } @@ -46,7 +46,7 @@ "options": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "input_error": "Ung\u00fcltige Eingaben, bitte \u00fcberpr\u00fcfen Sie Ihre Werte.", + "input_error": "Ung\u00fcltige Eingaben, bitte \u00fcberpr\u00fcfe deine Werte.", "select_single": "W\u00e4hle eine Option aus." }, "step": { @@ -56,7 +56,7 @@ "cat": "Ger\u00e4tekategorie (z. B. 0x10)", "subcat": "Ger\u00e4teunterkategorie (z. B. 0x0a)" }, - "description": "F\u00fcgen Sie eine Ger\u00e4te\u00fcberschreibung hinzu.", + "description": "F\u00fcge eine Ger\u00e4te\u00fcberschreibung hinzu.", "title": "Insteon" }, "add_x10": { @@ -66,7 +66,7 @@ "steps": "Dimmerstufen (nur f\u00fcr Lichtger\u00e4te, Voreinstellung 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "\u00c4ndern Sie das Insteon Hub-Passwort.", + "description": "\u00c4ndere das Insteon Hub-Passwort.", "title": "Insteon" }, "change_hub_config": { @@ -76,30 +76,30 @@ "port": "Port", "username": "Benutzername" }, - "description": "\u00c4ndern Sie die Verbindungsinformationen des Insteon-Hubs. Sie m\u00fcssen Home Assistant neu starten, nachdem Sie diese \u00c4nderung vorgenommen haben. Dies \u00e4ndert nicht die Konfiguration des Hubs selbst. Um die Konfiguration im Hub zu \u00e4ndern, verwenden Sie die Hub-App.", + "description": "\u00c4ndere die Verbindungsinformationen des Insteon-Hubs. Du musst Home Assistant neu starten, nachdem du diese \u00c4nderung vorgenommen hast. Dies \u00e4ndert nicht die Konfiguration des Hubs selbst. Um die Konfiguration im Hub zu \u00e4ndern, verwende die Hub-App.", "title": "Insteon" }, "init": { "data": { - "add_override": "F\u00fcgen Sie eine Ger\u00e4te\u00fcberschreibung hinzu.", - "add_x10": "F\u00fcgen Sie ein X10-Ger\u00e4t hinzu.", - "change_hub_config": "\u00c4ndern Sie die Konfiguration des Hubs.", - "remove_override": "Entfernen Sie eine Ger\u00e4te\u00fcbersteuerung.", - "remove_x10": "Entfernen Sie ein X10-Ger\u00e4t." + "add_override": "F\u00fcge eine Ger\u00e4te\u00fcberschreibung hinzu.", + "add_x10": "F\u00fcge ein X10-Ger\u00e4t hinzu.", + "change_hub_config": "\u00c4ndere die Konfiguration des Hubs.", + "remove_override": "Entferne eine Ger\u00e4te\u00fcbersteuerung.", + "remove_x10": "Entferne ein X10-Ger\u00e4t." }, - "description": "W\u00e4hlen Sie eine Option zum Konfigurieren aus.", + "description": "W\u00e4hle eine Option zum Konfigurieren aus.", "title": "Insteon" }, "remove_override": { "data": { - "address": "W\u00e4hlen Sie eine Ger\u00e4teadresse zum Entfernen" + "address": "W\u00e4hle eine Ger\u00e4teadresse zum Entfernen" }, "description": "Entfernen einer Ger\u00e4te\u00fcbersteuerung", "title": "Insteon" }, "remove_x10": { "data": { - "address": "W\u00e4hlen Sie eine Ger\u00e4teadresse zum Entfernen" + "address": "W\u00e4hle eine Ger\u00e4teadresse zum Entfernen" }, "description": "Ein X10-Ger\u00e4t entfernen", "title": "Insteon" diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 80497c0c874..289e75af55d 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option." + "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuche es erneut mit aktivierter SSL / TLS-Option." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/juicenet/translations/de.json b/homeassistant/components/juicenet/translations/de.json index fbdea4c321f..7a6b5cff541 100644 --- a/homeassistant/components/juicenet/translations/de.json +++ b/homeassistant/components/juicenet/translations/de.json @@ -13,7 +13,7 @@ "data": { "api_token": "API-Token" }, - "description": "Sie ben\u00f6tigen das API-Token von https://home.juice.net/Manage.", + "description": "Du ben\u00f6tigst das API-Token von https://home.juice.net/Manage.", "title": "Stelle eine Verbindung zu JuiceNet her" } } diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 80d47751006..9e486ed119b 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -31,13 +31,13 @@ "port": "Port", "ssl": "Verwendet ein SSL Zertifikat" }, - "description": "Kodi-Verbindungsinformationen. Bitte stellen Sie sicher, dass Sie \"Steuerung von Kodi \u00fcber HTTP zulassen\" in System/Einstellungen/Netzwerk/Dienste aktivieren." + "description": "Kodi-Verbindungsinformationen. Bitte stelle sicher, dass du \"Steuerung von Kodi \u00fcber HTTP zulassen\" in System/Einstellungen/Netzwerk/Dienste aktiviert hast." }, "ws_port": { "data": { "ws_port": "Port" }, - "description": "Der WebSocket-Port (in Kodi manchmal TCP-Port genannt). Um eine Verbindung \u00fcber WebSocket herzustellen, m\u00fcssen Sie unter System/Einstellungen/Netzwerk/Dienste \"Programme ... zur Steuerung von Kodi zulassen\" aktivieren. Wenn WebSocket nicht aktiviert ist, entfernen Sie den Port und lassen ihn leer." + "description": "Der WebSocket-Port (in Kodi manchmal TCP-Port genannt). Um eine Verbindung \u00fcber WebSocket herzustellen, musst du unter System/Einstellungen/Netzwerk/Dienste \"Programme ... zur Steuerung von Kodi zulassen\" aktivieren. Wenn WebSocket nicht aktiviert ist, entferne den Port und lasse ihn leer." } } }, diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 98862e85a8b..a438767a3f1 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -11,11 +11,11 @@ }, "step": { "confirm": { - "description": "Modell: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nSie k\u00f6nnen das I / O - und Bedienfeldverhalten in den Einstellungen der verbundenen Alarmzentrale konfigurieren.", + "description": "Modell: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nDu kannst das I / O - und Bedienfeldverhalten in den Einstellungen der verbundenen Alarmzentrale konfigurieren.", "title": "Konnected Device Bereit" }, "import_confirm": { - "description": "Ein Konnected Alarm Panel mit der ID {id} wurde in configuration.yaml entdeckt. Mit diesem Ablauf k\u00f6nnen Sie ihn in einen Konfigurationseintrag importieren.", + "description": "Ein Konnected Alarm Panel mit der ID {id} wurde in configuration.yaml entdeckt. Mit diesem Ablauf kannst du ihn in einen Konfigurationseintrag importieren.", "title": "Importieren von Konnected Ger\u00e4t" }, "user": { @@ -23,7 +23,7 @@ "host": "IP-Adresse", "port": "Port" }, - "description": "Bitte geben Sie die Hostinformationen f\u00fcr Ihr Konnected Panel ein." + "description": "Bitte gib die Hostinformationen f\u00fcr dein Konnected Panel ein." } } }, @@ -39,12 +39,12 @@ "step": { "options_binary": { "data": { - "inverse": "Invertieren Sie den \u00d6ffnungs- / Schlie\u00dfzustand", + "inverse": "Invertiere den \u00d6ffnungs- / Schlie\u00dfzustand", "name": "Name (optional)", "type": "Bin\u00e4rer Sensortyp" }, "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", - "title": "Konfigurieren Sie den Bin\u00e4rsensor" + "title": "Konfiguriere den Bin\u00e4rsensor" }, "options_digital": { "data": { @@ -53,7 +53,7 @@ "type": "Sensortyp" }, "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", - "title": "Konfigurieren Sie den digitalen Sensor" + "title": "Konfiguriere den digitalen Sensor" }, "options_io": { "data": { @@ -81,7 +81,7 @@ "out1": "OUT1" }, "description": "W\u00e4hlen Sie unten die Konfiguration der verbleibenden E / A. In den n\u00e4chsten Schritten k\u00f6nnen Sie detaillierte Optionen konfigurieren.", - "title": "Konfigurieren Sie Erweiterte I/O" + "title": "Konfiguriere Erweiterte I/O" }, "options_misc": { "data": { @@ -90,20 +90,20 @@ "discovery": "Reagieren auf Suchanfragen in Ihrem Netzwerk", "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" }, - "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel", + "description": "Bitte w\u00e4hle das gew\u00fcnschte Verhalten f\u00fcr dein Panel", "title": "Sonstiges konfigurieren" }, "options_switch": { "data": { "activation": "Ausgabe, wenn eingeschaltet", "momentary": "Impulsdauer (ms) (optional)", - "more_states": "Konfigurieren Sie zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone", + "more_states": "Konfiguriere zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone", "name": "Name (optional)", "pause": "Pause zwischen Impulsen (ms) (optional)", "repeat": "Mal wiederholen (-1 = unendlich) (optional)" }, "description": "Bitte w\u00e4hlen die Ausgabeoptionen f\u00fcr {zone} : Status {state}", - "title": "Konfigurieren Sie den schaltbaren Ausgang" + "title": "Konfiguriere den schaltbaren Ausgang" } } } diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index e87d4cc0bdb..4bd8e2a5931 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -15,7 +15,7 @@ "title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen." }, "link": { - "description": "Um ein Pairing mit {name} ({host}) durchzuf\u00fchren, dr\u00fccken Sie nach dem Absenden dieses Formulars die schwarze Taste auf der R\u00fcckseite der Br\u00fccke.", + "description": "Um ein Pairing mit {name} ({host}) durchzuf\u00fchren, dr\u00fccke nach dem Absenden dieses Formulars die schwarze Taste auf der R\u00fcckseite der Br\u00fccke.", "title": "Mit der Bridge verbinden" }, "user": { diff --git a/homeassistant/components/mazda/translations/de.json b/homeassistant/components/mazda/translations/de.json index 01dc3d97ffa..58409ec9b8a 100644 --- a/homeassistant/components/mazda/translations/de.json +++ b/homeassistant/components/mazda/translations/de.json @@ -5,7 +5,7 @@ "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "account_locked": "Konto gesperrt. Bitte versuchen Sie es sp\u00e4ter erneut.", + "account_locked": "Konto gesperrt. Bitte versuche es sp\u00e4ter erneut.", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" @@ -17,7 +17,7 @@ "password": "Passwort", "region": "Region" }, - "description": "Bitte geben Sie die E-Mail-Adresse und das Passwort ein, die Sie f\u00fcr die Anmeldung bei der MyMazda Mobile App verwenden.", + "description": "Bitte gib die E-Mail-Adresse und das Passwort ein, die du f\u00fcr die Anmeldung bei der MyMazda Mobile App verwendest.", "title": "Mazda Connected Services - Konto hinzuf\u00fcgen" } } diff --git a/homeassistant/components/melcloud/translations/de.json b/homeassistant/components/melcloud/translations/de.json index 54ae78f8680..7e4825587c7 100644 --- a/homeassistant/components/melcloud/translations/de.json +++ b/homeassistant/components/melcloud/translations/de.json @@ -15,7 +15,7 @@ "username": "E-Mail-Adresse" }, "description": "Verbinden Sie sich mit Ihrem MELCloud-Konto.", - "title": "Stellen Sie eine Verbindung zu MELCloud her" + "title": "Stelle eine Verbindung zu MELCloud her" } } } diff --git a/homeassistant/components/met_eireann/translations/de.json b/homeassistant/components/met_eireann/translations/de.json index a7e1ff9668b..7ce46bd36ab 100644 --- a/homeassistant/components/met_eireann/translations/de.json +++ b/homeassistant/components/met_eireann/translations/de.json @@ -11,7 +11,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name" }, - "description": "Geben Sie Ihren Standort ein, um Wetterdaten von der Met \u00c9ireann Public Weather Forecast API zu verwenden", + "description": "Gib deinen Standort ein, um Wetterdaten von der Met \u00c9ireann Public Weather Forecast API zu verwenden", "title": "Standort" } } diff --git a/homeassistant/components/meteoclimatic/translations/de.json b/homeassistant/components/meteoclimatic/translations/de.json index e23662146b2..c9b9ea90e61 100644 --- a/homeassistant/components/meteoclimatic/translations/de.json +++ b/homeassistant/components/meteoclimatic/translations/de.json @@ -12,7 +12,7 @@ "data": { "code": "Stationscode" }, - "description": "Geben Sie den Code der Meteoclimatic-Station ein (z. B. ESCAT4300000043206B)", + "description": "Gib den Code der Meteoclimatic-Station ein (z. B. ESCAT4300000043206B)", "title": "Meteoclimatic" } } diff --git a/homeassistant/components/modern_forms/translations/de.json b/homeassistant/components/modern_forms/translations/de.json index 3a289996ee9..9c8e1b21518 100644 --- a/homeassistant/components/modern_forms/translations/de.json +++ b/homeassistant/components/modern_forms/translations/de.json @@ -19,7 +19,7 @@ "description": "Einrichten Ihres Modern Forms Ventilator f\u00fcr die Integration in Home Assistant." }, "zeroconf_confirm": { - "description": "M\u00f6chten Sie den Modern Forms Ventilator mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du den Modern Forms Ventilator mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", "title": "Erkannter Modern Forms Ventilator" } } diff --git a/homeassistant/components/monoprice/translations/de.json b/homeassistant/components/monoprice/translations/de.json index 8f6d1d88196..66ae58cf757 100644 --- a/homeassistant/components/monoprice/translations/de.json +++ b/homeassistant/components/monoprice/translations/de.json @@ -18,7 +18,7 @@ "source_5": "Name der Quelle #5", "source_6": "Name der Quelle #6" }, - "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her" + "title": "Verbinden mit dem Ger\u00e4t" } } }, diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index 0a0ca806555..d565329b7cd 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -12,7 +12,7 @@ }, "step": { "hassio_confirm": { - "description": "M\u00f6chten Sie Home Assistant so konfigurieren, dass er sich mit dem motionEye-Dienst des Add-ons {addon} verbindet?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er sich mit dem motionEye-Dienst des Add-ons {addon} verbindet?", "title": "motionEye \u00fcber Home Assistant Add-on" }, "user": { diff --git a/homeassistant/components/motioneye/translations/id.json b/homeassistant/components/motioneye/translations/id.json new file mode 100644 index 00000000000..0278ac26195 --- /dev/null +++ b/homeassistant/components/motioneye/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_url": "URL tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "admin_password": "Kata Sandi Admin", + "admin_username": "Nama Pengguna Admin", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json index 5d8b5acdc79..d4bc7f35928 100644 --- a/homeassistant/components/myq/translations/de.json +++ b/homeassistant/components/myq/translations/de.json @@ -15,14 +15,14 @@ "password": "Passwort" }, "description": "Das Passwort f\u00fcr {username} ist nicht mehr g\u00fcltig.", - "title": "Authentifizieren Sie Ihr MyQ-Konto erneut" + "title": "Authentifiziere dein MyQ-Konto erneut" }, "user": { "data": { "password": "Passwort", "username": "Benutzername" }, - "title": "Stellen Sie eine Verbindung zum MyQ Gateway her" + "title": "Stelle eine Verbindung zum MyQ Gateway her" } } } diff --git a/homeassistant/components/nam/translations/id.json b/homeassistant/components/nam/translations/id.json new file mode 100644 index 00000000000..e289d14dd37 --- /dev/null +++ b/homeassistant/components/nam/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "device_unsupported": "Perangkat tidak didukung." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Siapkan integrasi Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 73106797381..e1a2c3c93cc 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -49,7 +49,7 @@ "mode": "Berechnung", "show_on_map": "Auf Karte anzeigen" }, - "description": "Konfigurieren Sie einen \u00f6ffentlichen Wettersensor f\u00fcr einen Bereich.", + "description": "Konfiguriere einen \u00f6ffentlichen Wettersensor f\u00fcr einen Bereich.", "title": "\u00d6ffentlicher Netatmo Wettersensor" }, "public_weather_areas": { diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json index e2a9a9bc739..94094f08037 100644 --- a/homeassistant/components/nexia/translations/de.json +++ b/homeassistant/components/nexia/translations/de.json @@ -15,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "title": "Stellen Sie eine Verbindung zu mynexia.com her" + "title": "Stelle eine Verbindung zu mynexia.com her" } } } diff --git a/homeassistant/components/nexia/translations/id.json b/homeassistant/components/nexia/translations/id.json index e6900bdffa1..0600315fc78 100644 --- a/homeassistant/components/nexia/translations/id.json +++ b/homeassistant/components/nexia/translations/id.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Merek", "password": "Kata Sandi", "username": "Nama Pengguna" }, diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index 91461416c60..02a301b1e78 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -16,7 +16,7 @@ "url": "URL" }, "description": "- URL: die Adresse Ihrer Nightscout-Instanz. Z.B.: https://myhomeassistant.duckdns.org:5423\n- API-Schl\u00fcssel (Optional): Nur verwenden, wenn Ihre Instanz gesch\u00fctzt ist (auth_default_roles != readable).", - "title": "Geben Sie Ihre Nightscout-Serverinformationen ein." + "title": "Gib deine Nightscout-Serverinformationen ein." } } } diff --git a/homeassistant/components/nmap_tracker/translations/id.json b/homeassistant/components/nmap_tracker/translations/id.json new file mode 100644 index 00000000000..d36ba84e8ac --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + } + }, + "options": { + "step": { + "init": { + "data": { + "track_new_devices": "Lacak perangkat baru" + } + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/de.json b/homeassistant/components/nuheat/translations/de.json index 8599f7fe1b5..47c1a90c459 100644 --- a/homeassistant/components/nuheat/translations/de.json +++ b/homeassistant/components/nuheat/translations/de.json @@ -16,8 +16,8 @@ "serial_number": "Seriennummer des Thermostats.", "username": "Benutzername" }, - "description": "Sie m\u00fcssen die numerische Seriennummer oder ID Ihres Thermostats erhalten, indem Sie sich bei https://MyNuHeat.com anmelden und Ihre Thermostate ausw\u00e4hlen.", - "title": "Stellen Sie eine Verbindung zu NuHeat her" + "description": "Du musst die numerische Seriennummer oder ID Ihres Thermostats erhalten, indem du dich bei https://MyNuHeat.com anmeldest und deine Thermostate ausw\u00e4hlst.", + "title": "Stelle eine Verbindung zu NuHeat her" } } } diff --git a/homeassistant/components/nws/translations/de.json b/homeassistant/components/nws/translations/de.json index b6899e34789..3851684def5 100644 --- a/homeassistant/components/nws/translations/de.json +++ b/homeassistant/components/nws/translations/de.json @@ -16,7 +16,7 @@ "station": "METAR Stationscode" }, "description": "Wenn kein METAR-Stationscode angegeben wird, werden der Breiten- und L\u00e4ngengrad verwendet, um die n\u00e4chstgelegene Station zu finden. Im Moment kann ein API-Schl\u00fcssel alles sein. Es wird empfohlen, eine g\u00fcltige E-Mail-Adresse zu verwenden.", - "title": "Stellen Sie eine Verbindung zum Nationalen Wetterdienst her" + "title": "Stelle eine Verbindung zum Nationalen Wetterdienst her" } } } diff --git a/homeassistant/components/omnilogic/translations/id.json b/homeassistant/components/omnilogic/translations/id.json index ed19cc68cf8..504d803fb13 100644 --- a/homeassistant/components/omnilogic/translations/id.json +++ b/homeassistant/components/omnilogic/translations/id.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Ofset pH (positif atau negatif)", "polling_interval": "Interval polling (dalam detik)" } } diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 109e6256791..4447b8dafea 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfen Sie die Profilkonfiguration auf Ihrem Ger\u00e4t.", + "no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfe die Profilkonfiguration auf deinem Ger\u00e4t.", "no_mac": "Die eindeutige ID f\u00fcr das ONVIF-Ger\u00e4t konnte nicht konfiguriert werden.", - "onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfen Sie die Protokolle auf weitere Informationen." + "onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfe die Protokolle auf weitere Informationen." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" @@ -16,7 +16,7 @@ "password": "Passwort", "username": "Benutzername" }, - "title": "Konfigurieren Sie die Authentifizierung" + "title": "Konfiguriere die Authentifizierung" }, "configure": { "data": { @@ -26,7 +26,7 @@ "port": "Port", "username": "Benutzername" }, - "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" + "title": "Konfiguriere das ONVIF-Ger\u00e4t" }, "configure_profile": { "data": { @@ -37,9 +37,9 @@ }, "device": { "data": { - "host": "W\u00e4hlen Sie das erkannte ONVIF-Ger\u00e4t aus" + "host": "W\u00e4hle das erkannte ONVIF-Ger\u00e4t aus" }, - "title": "W\u00e4hlen Sie ONVIF-Ger\u00e4t" + "title": "W\u00e4hle ein ONVIF-Ger\u00e4t" }, "manual_input": { "data": { @@ -47,13 +47,13 @@ "name": "Name", "port": "Port" }, - "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" + "title": "Konfiguriere das ONVIF-Ger\u00e4t" }, "user": { "data": { "auto": "Automatisch suchen" }, - "description": "Wenn Sie auf Senden klicken, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stellen Sie sicher, dass ONVIF in der Konfiguration Ihrer Kamera aktiviert ist.", + "description": "Wenn du auf Senden klickst, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stelle sicher, dass ONVIF in der Konfiguration deiner Kamera aktiviert ist.", "title": "ONVIF-Ger\u00e4tekonfiguration" } } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index da0305f633d..7b2806693f0 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -17,7 +17,7 @@ "mode": "Modus", "name": "Name der Integration" }, - "description": "Richten Sie die OpenWeatherMap-Integration ein. Zum Generieren des API-Schl\u00fcssels gehen Sie auf https://openweathermap.org/appid", + "description": "Richte die OpenWeatherMap-Integration ein. Zum Generieren des API-Schl\u00fcssels gehe auf https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index de86a7adf14..74d3007430f 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -11,7 +11,7 @@ "data": { "password": "Passwort" }, - "description": "Die Authentifizierung f\u00fcr OVO Energy ist fehlgeschlagen. Bitte geben Sie Ihre aktuellen Anmeldedaten ein.", + "description": "Die Authentifizierung f\u00fcr OVO Energy ist fehlgeschlagen. Bitte gib deine aktuellen Anmeldedaten ein.", "title": "Erneute Authentifizierung" }, "user": { diff --git a/homeassistant/components/panasonic_viera/translations/de.json b/homeassistant/components/panasonic_viera/translations/de.json index 71090830714..73143624e68 100644 --- a/homeassistant/components/panasonic_viera/translations/de.json +++ b/homeassistant/components/panasonic_viera/translations/de.json @@ -23,7 +23,7 @@ "name": "Name" }, "description": "Gib die IP-Adresse deines Panasonic Viera TV ein", - "title": "Richten Sie Ihr Fernsehger\u00e4t ein" + "title": "Richte dein Fernsehger\u00e4t ein" } } } diff --git a/homeassistant/components/picnic/translations/id.json b/homeassistant/components/picnic/translations/id.json new file mode 100644 index 00000000000..0455a5b3b5e --- /dev/null +++ b/homeassistant/components/picnic/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "country_code": "Kode Negara", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index 6e02096d0b4..ea079631e1c 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -16,10 +16,10 @@ "step": { "api_method": { "data": { - "token": "F\u00fcgen Sie hier das Auth Token ein", + "token": "F\u00fcge hier das Auth Token ein", "use_webhook": "Webhook verwenden" }, - "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{device_type}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer", + "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{device_type}** \n\nWenn du lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chtest, setze bitte einen Haken und lasse das Auth Token leer", "title": "API-Methode ausw\u00e4hlen" }, "user": { diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index 2ba14e65f85..ba2a2c52229 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -35,7 +35,7 @@ "title": "Plex-Server ausw\u00e4hlen" }, "user": { - "description": "Gehen Sie zu [plex.tv] (https://plex.tv), um einen Plex-Server zu verbinden", + "description": "Gehe zu [plex.tv] (https://plex.tv), um einen Plex-Server zu verbinden", "title": "Plex Media Server" }, "user_advanced": { diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 9e2836202df..4e4bc8baeee 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -25,7 +25,7 @@ "username": "Smile-Benutzername" }, "description": "Bitte eingeben", - "title": "Stellen Sie eine Verbindung zu Smile her" + "title": "Stelle eine Verbindung zu Smile her" } } }, diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 88b0473232f..8ac8a1a5b1d 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -18,7 +18,7 @@ "password": "Passwort" }, "description": "Das Kennwort ist in der Regel die letzten 5 Zeichen der Seriennummer des Backup Gateway und kann in der Tesla-App gefunden werden oder es sind die letzten 5 Zeichen des Kennworts, das sich in der T\u00fcr f\u00fcr Backup Gateway 2 befindet.", - "title": "Stellen Sie eine Verbindung zur Powerwall her" + "title": "Stelle eine Verbindung zur Powerwall her" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 04f5ddb8fba..626382fcb6f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -11,7 +11,7 @@ "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Geltender Tarif nach geografischer Zone" }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). ", + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen findest du in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). ", "title": "Sensoreinrichtung" } } @@ -24,7 +24,7 @@ "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Geltender Tarif nach geografischer Zone" }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten.\nEine genauere Erkl\u00e4rung finden Sie in den [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten.\nEine genauere Erkl\u00e4rung findest du in den [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "title": "Sensoreinrichtung" } } diff --git a/homeassistant/components/rachio/translations/de.json b/homeassistant/components/rachio/translations/de.json index 9acd92ce40d..02a61e8f573 100644 --- a/homeassistant/components/rachio/translations/de.json +++ b/homeassistant/components/rachio/translations/de.json @@ -14,7 +14,7 @@ "api_key": "API-Schl\u00fcssel" }, "description": "Du ben\u00f6tigst den API-Schl\u00fcssel von https://app.rach.io/. Gehe in die Einstellungen und klicke auf \"API-SCHL\u00dcSSEL ANFORDERN\".", - "title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her" + "title": "Stelle eine Verbindung zu deinem Rachio-Ger\u00e4t her" } } }, diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json index 482ffb75278..bcbb9126b2a 100644 --- a/homeassistant/components/rainmachine/translations/id.json +++ b/homeassistant/components/rainmachine/translations/id.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autentikasi tidak valid" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/recollect_waste/translations/de.json b/homeassistant/components/recollect_waste/translations/de.json index fdeab56f54e..750f84dd210 100644 --- a/homeassistant/components/recollect_waste/translations/de.json +++ b/homeassistant/components/recollect_waste/translations/de.json @@ -19,7 +19,7 @@ "step": { "init": { "data": { - "friendly_name": "Verwenden Sie freundliche Namen f\u00fcr Pickup-Typen (wenn m\u00f6glich)" + "friendly_name": "Verwende freundliche Namen f\u00fcr Pickup-Typen (wenn m\u00f6glich)" }, "title": "Recollect Waste konfigurieren" } diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index 424a93f3eb7..a5ebcab51b5 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -27,7 +27,7 @@ "armed_home": "Aktiv, zu Hause", "armed_night": "Aktiv, Nacht" }, - "description": "W\u00e4hlen Sie aus, in welchen Zustand Ihr Risco-Alarm versetzt werden soll, wenn Sie den Alarm des Home Assistant scharf schalten", + "description": "W\u00e4hle, in welchen Zustand dein Risco-Alarm versetzt werden soll, wenn du den Alarm des Home Assistant scharf schaltest", "title": "Home Assistant Zust\u00e4nde den Risco Zust\u00e4nden zuordnen" }, "init": { @@ -47,7 +47,7 @@ "arm": "Aktiv, abwesend", "partial_arm": "Teilweise aktiv (STAY)" }, - "description": "W\u00e4hlen Sie aus, welchen Zustand Ihr Home Assistant-Alarm f\u00fcr jeden von Risco gemeldeten Zustand melden soll", + "description": "W\u00e4hle aus, welchen Zustand dein Home Assistant-Alarm f\u00fcr jeden von Risco gemeldeten Zustand melden soll", "title": "Risco-Zust\u00e4nde den Home Assistant-Zust\u00e4nden zuordnen" } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/de.json b/homeassistant/components/rituals_perfume_genie/translations/de.json index 72f18702457..5edc9f60dd0 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/de.json +++ b/homeassistant/components/rituals_perfume_genie/translations/de.json @@ -14,7 +14,7 @@ "email": "E-Mail", "password": "Passwort" }, - "title": "Verbinden Sie sich mit Ihrem Rituals-Konto" + "title": "Verbinden mit deinem Rituals-Konto" } } } diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 77cd1b94b6e..5f72b4bdd9b 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -19,14 +19,14 @@ "one": "eins", "other": "andere" }, - "description": "M\u00f6chten Sie {name} einrichten?", + "description": "M\u00f6chtest du {name} einrichten?", "title": "Roku" }, "user": { "data": { "host": "Host" }, - "description": "Geben Sie Ihre Roku-Informationen ein." + "description": "Gib deine Roku-Informationen ein." } } } diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 193469008e2..eb907115188 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -34,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "Es wurde kein Roomba oder Braava in Ihrem Netzwerk entdeckt. Die BLID ist der Teil des Ger\u00e4te-Hostnamens nach `iRobot-` oder `Roomba-`. Bitte folgen Sie den Schritten, die in der Dokumentation unter: {auth_help_url}", + "description": "Es wurde kein Roomba oder Braava in Ihrem Netzwerk entdeckt. Die BLID ist der Teil des Ger\u00e4te-Hostnamens nach `iRobot-` oder `Roomba-`. Bitte folge den Schritten in der Dokumentation unter: {auth_help_url}", "title": "Manuell mit dem Ger\u00e4t verbinden" }, "user": { diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index cd4aae46adc..4eadf9c363a 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -9,14 +9,14 @@ }, "step": { "link": { - "description": "Sie m\u00fcssen den Home Assistant in Roon autorisieren. Nachdem Sie auf \"Submit\" geklickt haben, gehen Sie zur Roon Core-Anwendung, \u00f6ffnen Sie die Einstellungen und aktivieren Sie HomeAssistant auf der Registerkarte \"Extensions\".", + "description": "Du musst den Home Assistant in Roon autorisieren. Nachdem du auf \"Submit\" geklickt hast, gehe zur Roon Core-Anwendung, \u00f6ffne die Einstellungen und aktiviere HomeAssistant auf der Registerkarte \"Extensions\".", "title": "HomeAssistant in Roon autorisieren" }, "user": { "data": { "host": "Host" }, - "description": "Roon-Server konnte nicht gefunden werden, bitte geben Sie den Hostnamen oder die IP ein." + "description": "Roon-Server konnte nicht gefunden werden, bitte gib den Hostnamen oder die IP ein." } } } diff --git a/homeassistant/components/rpi_power/translations/de.json b/homeassistant/components/rpi_power/translations/de.json index cc830bce135..ada0c101a52 100644 --- a/homeassistant/components/rpi_power/translations/de.json +++ b/homeassistant/components/rpi_power/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Die f\u00fcr diese Komponente ben\u00f6tigte Systemklasse konnte nicht gefunden werden. Stellen Sie sicher, dass Ihr Kernel aktuell ist und die Hardware unterst\u00fctzt wird", + "no_devices_found": "Die f\u00fcr diese Komponente ben\u00f6tigte Systemklasse konnte nicht gefunden werden. Stelle sicher, dass dein Kernel aktuell ist und die Hardware unterst\u00fctzt wird", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index 710443bc24f..5e79709f5bd 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -20,7 +20,7 @@ "title": "Samsung TV" }, "reauth_confirm": { - "description": "Akzeptieren Sie nach dem Absenden die Meldung auf {device}, das eine Autorisierung innerhalb von 30 Sekunden anfordert." + "description": "Akzeptiere nach dem Absenden die Meldung auf {device}, das eine Autorisierung innerhalb von 30 Sekunden anfordert." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json index 7d0f5982a65..0b8bbe60150 100644 --- a/homeassistant/components/samsungtv/translations/id.json +++ b/homeassistant/components/samsungtv/translations/id.json @@ -5,7 +5,12 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa setelan TV Anda untuk mengotorisasi Home Assistant.", "cannot_connect": "Gagal terhubung", - "not_supported": "Perangkat TV Samsung ini saat ini tidak didukung." + "not_supported": "Perangkat TV Samsung ini saat ini tidak didukung.", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa setelan TV Anda untuk mengotorisasi Home Assistant." }, "flow_title": "TV Samsung: {model}", "step": { diff --git a/homeassistant/components/select/translations/id.json b/homeassistant/components/select/translations/id.json new file mode 100644 index 00000000000..f042ddf3218 --- /dev/null +++ b/homeassistant/components/select/translations/id.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Ubah opsi {entity_name}" + }, + "condition_type": { + "selected_option": "Opsi terpilih {entity_name}" + }, + "trigger_type": { + "current_option_changed": "Opsi {entity_name} berubah" + } + }, + "title": "Pilihan" +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json index 9d4845ece79..27cfcc5e5dc 100644 --- a/homeassistant/components/sense/translations/de.json +++ b/homeassistant/components/sense/translations/de.json @@ -14,7 +14,7 @@ "email": "E-Mail-Adresse", "password": "Passwort" }, - "title": "Stellen Sie eine Verbindung zu Ihrem Sense Energy Monitor her" + "title": "Stelle eine Verbindung zu deinem Sense Energy Monitor her" } } } diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 70053a86144..7e25744420e 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -12,7 +12,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "M\u00f6chten Sie das {model} bei {host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Sie k\u00f6nnen das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." + "description": "M\u00f6chtest du das {model} bei {host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Du kannst das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." }, "credentials": { "data": { diff --git a/homeassistant/components/shopping_list/translations/de.json b/homeassistant/components/shopping_list/translations/de.json index 68372e9f4ac..f76d537349f 100644 --- a/homeassistant/components/shopping_list/translations/de.json +++ b/homeassistant/components/shopping_list/translations/de.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie die Einkaufsliste konfigurieren?", + "description": "M\u00f6chtest du die Einkaufsliste konfigurieren?", "title": "Einkaufsliste" } } diff --git a/homeassistant/components/sia/translations/de.json b/homeassistant/components/sia/translations/de.json index 6da5a2c4750..d2c9fc05040 100644 --- a/homeassistant/components/sia/translations/de.json +++ b/homeassistant/components/sia/translations/de.json @@ -1,9 +1,9 @@ { "config": { "error": { - "invalid_account_format": "Das Konto ist kein Hex-Wert. Bitte verwenden Sie nur 0-9 und A-F.", + "invalid_account_format": "Das Konto ist kein Hex-Wert. Bitte verwende nur 0-9 und A-F.", "invalid_account_length": "Das Konto hat nicht die richtige L\u00e4nge. Es muss zwischen 3 und 16 Zeichen lang sein.", - "invalid_key_format": "Der Schl\u00fcssel ist kein Hex-Wert, bitte verwenden Sie nur 0-9 und A-F.", + "invalid_key_format": "Der Schl\u00fcssel ist kein Hex-Wert, bitte verwende nur 0-9 und A-F.", "invalid_key_length": "Der Schl\u00fcssel hat nicht die richtige L\u00e4nge. Er muss 16, 24 oder 32 Hex-Zeichen lang sein.", "invalid_ping": "Das Ping-Intervall muss zwischen 1 und 1440 Minuten liegen.", "invalid_zones": "Es muss mindestens eine Zone vorhanden sein.", @@ -41,7 +41,7 @@ "ignore_timestamps": "Ignorieren der Zeitstempelpr\u00fcfung der SIA-Ereignisse", "zones": "Anzahl an Zonen f\u00fcr das Konto" }, - "description": "Stellen Sie die Optionen f\u00fcr das Konto {account} ein:", + "description": "Stelle die Optionen f\u00fcr das Konto {account} ein:", "title": "Optionen f\u00fcr das SIA-Setup." } } diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 046c46c01ac..e4966581fac 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -38,7 +38,7 @@ "data": { "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)" }, - "title": "Konfigurieren Sie SimpliSafe" + "title": "Konfiguriere SimpliSafe" } } } diff --git a/homeassistant/components/sma/translations/de.json b/homeassistant/components/sma/translations/de.json index c17271cae20..7ec8fe06d0f 100644 --- a/homeassistant/components/sma/translations/de.json +++ b/homeassistant/components/sma/translations/de.json @@ -20,7 +20,7 @@ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Gib deine SMA-Ger\u00e4teinformationen ein.", - "title": "Richten Sie SMA Solar ein" + "title": "Richte SMA Solar ein" } } } diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index 3c8d096403c..21fd056982e 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -19,14 +19,14 @@ "data": { "access_token": "Zugriffs-Token" }, - "description": "Bitte geben Sie ein SmartThings [Personal Access Token] ({token_url}) ein, das gem\u00e4\u00df den [Anweisungen] ({component_url}) erstellt wurde. Dies wird zur Erstellung der Home Assistant-Integration in Ihrem SmartThings-Konto verwendet.", + "description": "Bitte gib ein SmartThings [Personal Access Token] ({token_url}) ein, das gem\u00e4\u00df den [Anweisungen] ({component_url}) erstellt wurde. Dies wird zur Erstellung der Home Assistant-Integration in deinem SmartThings-Konto verwendet.", "title": "Gib den pers\u00f6nlichen Zugangstoken an" }, "select_location": { "data": { "location_id": "Standort" }, - "description": "Bitte w\u00e4hlen Sie den SmartThings-Standort aus, den Sie Home Assistant hinzuf\u00fcgen m\u00f6chten. Wir \u00f6ffnen dann ein neues Fenster und bitten Sie, sich anzumelden und die Installation der Home Assistant-Integration am ausgew\u00e4hlten Standort zu autorisieren.", + "description": "Bitte w\u00e4hle den SmartThings-Standort aus, den du Home Assistant hinzuf\u00fcgen m\u00f6chtest. Wir \u00f6ffnen dann ein neues Fenster und bitten dich, sich anzumelden und die Installation der Home Assistant-Integration am ausgew\u00e4hlten Standort zu autorisieren.", "title": "Standort ausw\u00e4hlen" }, "user": { diff --git a/homeassistant/components/smarttub/translations/id.json b/homeassistant/components/smarttub/translations/id.json index 672310e0d2f..bf32b29d1e7 100644 --- a/homeassistant/components/smarttub/translations/id.json +++ b/homeassistant/components/smarttub/translations/id.json @@ -8,6 +8,9 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { + "reauth_confirm": { + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/soma/translations/de.json b/homeassistant/components/soma/translations/de.json index 79cd15df3be..17cc5c1899d 100644 --- a/homeassistant/components/soma/translations/de.json +++ b/homeassistant/components/soma/translations/de.json @@ -4,7 +4,7 @@ "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "connection_error": "Verbindung zu SOMA Connect fehlgeschlagen.", - "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folge der Dokumentation.", "result_error": "SOMA Connect hat mit einem Fehlerstatus geantwortet." }, "create_entry": { diff --git a/homeassistant/components/songpal/translations/de.json b/homeassistant/components/songpal/translations/de.json index b9b7fae3f28..ae1695eaa2d 100644 --- a/homeassistant/components/songpal/translations/de.json +++ b/homeassistant/components/songpal/translations/de.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "init": { - "description": "M\u00f6chten Sie {name} ({host}) einrichten?" + "description": "M\u00f6chtest du {name} ({host}) einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index f92db780e82..69984b39798 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", - "reauth_account_mismatch": "Das Spotify-Konto, mit dem Sie sich authentifiziert haben, stimmt nicht mit dem Konto \u00fcberein, f\u00fcr das Sie sich erneut authentifizieren m\u00fcssen." + "reauth_account_mismatch": "Das Spotify-Konto, mit dem du dich authentifiziert hast, stimmt nicht mit dem Konto \u00fcberein, f\u00fcr das du dich erneut authentifizieren musst." }, "create_entry": { "default": "Erfolgreich mit Spotify authentifiziert." diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index ac953654565..b1e55c3ade5 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "update_enabled": "Aktivieren Sie die Fahrzeugabfrage" + "update_enabled": "Aktiviere die Fahrzeugabfrage" }, "description": "Wenn diese Option aktiviert ist, sendet die Fahrzeugabfrage alle 2 Stunden einen Fernbefehl an Ihr Fahrzeug, um neue Sensordaten zu erhalten. Ohne Fahrzeugabfrage werden neue Sensordaten nur empfangen, wenn das Fahrzeug automatisch Daten sendet (normalerweise nach dem Abstellen des Motors).", "title": "Subaru Starlink Optionen" diff --git a/homeassistant/components/syncthing/translations/id.json b/homeassistant/components/syncthing/translations/id.json new file mode 100644 index 00000000000..2aa4f701c95 --- /dev/null +++ b/homeassistant/components/syncthing/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 932cc42db1d..766fc7f6e91 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -6,8 +6,8 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "missing_data": "Fehlende Daten: Bitte versuchen Sie es sp\u00e4ter noch einmal oder eine andere Konfiguration", - "otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code", + "missing_data": "Fehlende Daten: Bitte versuche es sp\u00e4ter noch einmal oder eine andere Konfiguration", + "otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuche es erneut mit einem neuen Code", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/system_bridge/translations/id.json b/homeassistant/components/system_bridge/translations/id.json new file mode 100644 index 00000000000..9995253cbca --- /dev/null +++ b/homeassistant/components/system_bridge/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "api_key": "Kunci API" + }, + "description": "Masukkan Kunci API yang Anda atur dalam konfigurasi Anda untuk {name}." + }, + "user": { + "data": { + "api_key": "Kunci API", + "host": "Host", + "port": "Port" + }, + "description": "Masukkan detail koneksi Anda." + } + } + }, + "title": "Jembatan Sistem" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index 8d49c9d9e61..d6339bbf20b 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -13,7 +13,7 @@ "data": { "access_token": "Zugriffs-Token" }, - "description": "Geben Sie Ihr Zugangsk\u00fcrzel von https://developer.tibber.com/settings/accesstoken ein.", + "description": "Gib dein Zugangsk\u00fcrzel von https://developer.tibber.com/settings/accesstoken ein.", "title": "Tibber" } } diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index c76bab5ef91..8f7531896f8 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -13,11 +13,11 @@ "data": { "agreement": "Vereinbarung" }, - "description": "W\u00e4hlen Sie die Vereinbarungsadresse aus, die du hinzuf\u00fcgen m\u00f6chtest.", + "description": "W\u00e4hle die Vereinbarungsadresse aus, die du hinzuf\u00fcgen m\u00f6chtest.", "title": "W\u00e4hle deine Vereinbarung" }, "pick_implementation": { - "title": "W\u00e4hlen Sie Ihren Mandanten f\u00fcr die Authentifizierung aus" + "title": "W\u00e4hle deinen Mandanten f\u00fcr die Authentifizierung aus" } } } diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index b6543752043..c435169c804 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -14,7 +14,7 @@ "location": "Standort", "usercode": "Benutzercode" }, - "description": "Geben Sie den Benutzercode f\u00fcr den Benutzer {location_id} an dieser Stelle ein", + "description": "Gib den Benutzercode f\u00fcr den Benutzer {location_id} an dieser Stelle ein", "title": "Standort-Benutzercodes" }, "reauth_confirm": { diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 289d2661485..ac5a7e77b19 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -53,11 +53,11 @@ "init": { "data": { "discovery_interval": "Abfrageintervall f\u00fcr Ger\u00e4teabruf in Sekunden", - "list_devices": "W\u00e4hlen Sie die zu konfigurierenden Ger\u00e4te aus oder lassen Sie sie leer, um die Konfiguration zu speichern", - "query_device": "W\u00e4hlen Sie ein Ger\u00e4t aus, das die Abfragemethode f\u00fcr eine schnellere Statusaktualisierung verwendet.", + "list_devices": "W\u00e4hle die zu konfigurierenden Ger\u00e4te aus oder lasse sie leer, um die Konfiguration zu speichern", + "query_device": "W\u00e4hle ein Ger\u00e4t aus, das die Abfragemethode f\u00fcr eine schnellere Statusaktualisierung verwendet.", "query_interval": "Ger\u00e4teabrufintervall in Sekunden" }, - "description": "Stellen Sie das Abfrageintervall nicht zu niedrig ein, sonst schlagen die Aufrufe fehl und erzeugen eine Fehlermeldung im Protokoll", + "description": "Stelle das Abfrageintervall nicht zu niedrig ein, sonst schlagen die Aufrufe fehl und erzeugen eine Fehlermeldung im Protokoll", "title": "Tuya-Optionen konfigurieren" } } diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index f1f2fdd3627..bed0cc289ab 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -33,19 +33,19 @@ "dpi_restrictions": "Zulassen der Steuerung von DPI-Einschr\u00e4nkungsgruppen", "poe_clients": "POE-Kontrolle von Clients zulassen" }, - "description": "Konfigurieren Sie Client-Steuerelemente \n\nErstellen Sie Switches f\u00fcr Seriennummern, f\u00fcr die Sie den Netzwerkzugriff steuern m\u00f6chten.", + "description": "Konfiguriere Client-Steuerelemente \n\nErstelle Switches f\u00fcr Seriennummern, f\u00fcr die du den Netzwerkzugriff steuern m\u00f6chtest.", "title": "UniFi-Optionen 2/3" }, "device_tracker": { "data": { "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", "ignore_wired_bug": "Deaktivieren der kabelgebundenen UniFi-Fehlerlogik", - "ssid_filter": "W\u00e4hlen Sie SSIDs zur Verfolgung von drahtlosen Clients aus", + "ssid_filter": "W\u00e4hle SSIDs zur Verfolgung von drahtlosen Clients aus", "track_clients": "Nachverfolgen von Netzwerkclients", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" }, - "description": "Konfigurieren Sie die Ger\u00e4teverfolgung", + "description": "Konfiguriere die Ger\u00e4teverfolgung", "title": "UniFi-Optionen 1/3" }, "init": { @@ -60,14 +60,14 @@ "track_clients": "Netzwerger\u00e4te \u00fcberwachen", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)" }, - "description": "Konfigurieren Sie die UniFi-Integration" + "description": "Konfiguriere die UniFi-Integration" }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients", "allow_uptime_sensors": "Uptime-Sensoren f\u00fcr Netzwerk-Clients" }, - "description": "Konfigurieren Sie Statistiksensoren", + "description": "Konfiguriere die Statistiksensoren", "title": "UniFi-Optionen 3/3" } } diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index a43700ba236..51be5a6b506 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "ssdp_confirm": { - "description": "M\u00f6chten Sie dieses UPnP/IGD-Ger\u00e4t einrichten?" + "description": "M\u00f6chtest du dieses UPnP/IGD-Ger\u00e4t einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/upnp/translations/id.json b/homeassistant/components/upnp/translations/id.json index 463e61f271c..3a953ba62a9 100644 --- a/homeassistant/components/upnp/translations/id.json +++ b/homeassistant/components/upnp/translations/id.json @@ -13,9 +13,19 @@ "user": { "data": { "scan_interval": "Interval pembaruan (dalam detik, minimal 30)", + "unique_id": "Perangkat", "usn": "Perangkat" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pembaruan (dalam detik, minimal 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json index 8f20c074ff4..01c4a894c24 100644 --- a/homeassistant/components/vilfo/translations/de.json +++ b/homeassistant/components/vilfo/translations/de.json @@ -15,7 +15,7 @@ "host": "Host" }, "description": "Richten Sie die Vilfo Router-Integration ein. Sie ben\u00f6tigen Ihren Vilfo Router-Hostnamen / Ihre IP-Adresse und ein API-Zugriffstoken. Weitere Informationen zu dieser Integration und wie Sie diese Details erhalten, finden Sie unter: https://www.home-assistant.io/integrations/vilfo", - "title": "Stellen Sie eine Verbindung zum Vilfo Router her" + "title": "Stelle eine Verbindung zum Vilfo Router her" } } } diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index 28cb0d2c0b2..0ed6fac9e83 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -8,15 +8,15 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst.", - "existing_config_entry_found": "Ein bestehender VIZIO SmartCast-Ger\u00e4t Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Sie m\u00fcssen den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." + "existing_config_entry_found": "Ein bestehender VIZIO SmartCast-Ger\u00e4t Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Du musst den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." }, "step": { "pair_tv": { "data": { "pin": "PIN-Code" }, - "description": "Ihr Fernseher sollte einen Code anzeigen. Geben Sie diesen Code in das Formular ein und fahren Sie mit dem n\u00e4chsten Schritt fort, um die Kopplung abzuschlie\u00dfen.", - "title": "Schlie\u00dfen Sie den Pairing-Prozess ab" + "description": "Dein Fernseher sollte einen Code anzeigen. Gib diesen Code in das Formular ein und fahre mit dem n\u00e4chsten Schritt fort, um die Kopplung abzuschlie\u00dfen.", + "title": "Schlie\u00dfe den Pairing-Prozess ab" }, "pairing_complete": { "description": "Dein VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.", diff --git a/homeassistant/components/wallbox/translations/id.json b/homeassistant/components/wallbox/translations/id.json new file mode 100644 index 00000000000..8fa55e63051 --- /dev/null +++ b/homeassistant/components/wallbox/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json index 2af713eb5d1..42ef4698151 100644 --- a/homeassistant/components/waze_travel_time/translations/de.json +++ b/homeassistant/components/waze_travel_time/translations/de.json @@ -14,7 +14,7 @@ "origin": "Startort", "region": "Region" }, - "description": "Geben Sie f\u00fcr Ursprung und Ziel die Adresse oder die GPS-Koordinaten des Standorts ein (GPS-Koordinaten m\u00fcssen durch ein Komma getrennt werden). Sie k\u00f6nnen auch eine Entity-ID eingeben, die diese Informationen in ihrem Zustand bereitstellt, eine Entity-ID mit den Attributen Breitengrad und L\u00e4ngengrad oder einen Zonen-Namen." + "description": "Gib f\u00fcr Ursprung und Ziel die Adresse oder die GPS-Koordinaten des Standorts ein (GPS-Koordinaten m\u00fcssen durch ein Komma getrennt werden). Du kannst auch eine Entity-ID eingeben, die diese Informationen in ihrem Zustand bereitstellt, eine Entity-ID mit den Attributen Breitengrad und L\u00e4ngengrad oder einen Zonen-Namen." } } }, @@ -31,7 +31,7 @@ "units": "Einheiten", "vehicle_type": "Fahrzeugtyp" }, - "description": "Mit den \"Substring\"-Eintr\u00e4gen k\u00f6nnen Sie die Integration zwingen, eine bestimmte Route zu verwenden oder eine bestimmte Route bei der Zeitreiseberechnung zu vermeiden." + "description": "Mit den \"Substring\"-Eintr\u00e4gen kannst du die Integration zwingen, eine bestimmte Route zu verwenden oder eine bestimmte Route bei der Zeitreiseberechnung zu vermeiden." } } }, diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index 851defe0e32..546f8cec7b5 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "M\u00f6chten Sie WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}", + "description": "M\u00f6chtest du WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 13f20fe29b2..01a70fe88d6 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -41,7 +41,7 @@ "name": "Name des Ger\u00e4ts", "token": "API-Token" }, - "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr eine Anleitung. Dieser unterscheidet sich vom API-Token, den die Xiaomi Aqara-Integration nutzt.", + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr eine Anleitung. Dieser unterscheidet sich vom API-Token, den die Xiaomi Aqara-Integration nutzt.", "title": "Herstellen einer Verbindung mit einem Xiaomi Miio-Ger\u00e4t oder Xiaomi Gateway" }, "gateway": { @@ -50,7 +50,7 @@ "name": "Name des Gateways", "token": "API-Token" }, - "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, "manual": { @@ -76,7 +76,7 @@ "data": { "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, - "description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.", + "description": "W\u00e4hle aus, mit welchem Ger\u00e4t du eine Verbindung herstellen m\u00f6chtest.", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json index d55e19980a7..f893f7b06aa 100644 --- a/homeassistant/components/xiaomi_miio/translations/id.json +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -11,6 +11,23 @@ }, "flow_title": "Xiaomi Miio: {name}", "step": { + "cloud": { + "data": { + "cloud_country": "Negara server cloud", + "cloud_password": "Kata sandi cloud", + "cloud_username": "Nama pengguna cloud", + "manual": "Konfigurasi secara manual (tidak disarankan)" + }, + "description": "Masuk ke cloud Xiaomi Miio, lihat https://www.openhab.org/addons/bindings/miio/#country-servers untuk menemukan server cloud yang digunakan.", + "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Model perangkat" + }, + "description": "Pilih model perangkat secara manual dari model yang didukung.", + "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + }, "device": { "data": { "host": "Alamat IP", @@ -30,6 +47,23 @@ "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.", "title": "Hubungkan ke Xiaomi Gateway" }, + "manual": { + "data": { + "host": "Alamat IP", + "token": "Token API" + }, + "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + }, + "reauth_confirm": { + "title": "Autentikasi Ulang Integrasi" + }, + "select": { + "data": { + "select_device": "Perangkat Miio" + }, + "description": "Pilih perangkat Xiaomi Miio untuk disiapkan.", + "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + }, "user": { "data": { "gateway": "Hubungkan ke Xiaomi Gateway" @@ -38,5 +72,16 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "step": { + "init": { + "data": { + "cloud_subdevices": "Gunakan cloud untuk mendapatkan subperangkat yang tersambung" + }, + "description": "Tentukan pengaturan opsional", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/id.json b/homeassistant/components/yamaha_musiccast/translations/id.json new file mode 100644 index 00000000000..72a79af2041 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 0c9e6d2a154..e0bf573f95e 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -10,7 +10,7 @@ "flow_title": "{model} {host}", "step": { "discovery_confirm": { - "description": "M\u00f6chten Sie {model} ({host}) einrichten?" + "description": "M\u00f6chtest du {model} ({host}) einrichten?" }, "pick_device": { "data": { @@ -35,7 +35,7 @@ "transition": "\u00dcbergangszeit (ms)", "use_music_mode": "Musik-Modus aktivieren" }, - "description": "Wenn Sie das Modell leer lassen, wird es automatisch erkannt." + "description": "Wenn du das Modell leer l\u00e4sst, wird es automatisch erkannt." } } } diff --git a/homeassistant/components/yeelight/translations/id.json b/homeassistant/components/yeelight/translations/id.json index 0c81739095d..3b2f0273ae3 100644 --- a/homeassistant/components/yeelight/translations/id.json +++ b/homeassistant/components/yeelight/translations/id.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Gagal terhubung" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {model} ({host})?" + }, "pick_device": { "data": { "device": "Perangkat" diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 48a84e712f2..3d66cc63071 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -12,7 +12,7 @@ "data": { "radio_type": "Funktyp" }, - "description": "W\u00e4hlen Sie einen Typ Ihres Zigbee-Funks", + "description": "W\u00e4hle den Typ deines Zigbee-Funks", "title": "Funktyp" }, "port_config": { @@ -21,14 +21,14 @@ "flow_control": "Datenflusskontrolle", "path": "Serieller Ger\u00e4tepfad" }, - "description": "Geben Sie die portspezifischen Einstellungen ein", + "description": "Gib die portspezifischen Einstellungen ein", "title": "Einstellungen" }, "user": { "data": { "path": "Serieller Ger\u00e4tepfad" }, - "description": "W\u00e4hlen Sie die serielle Schnittstelle f\u00fcr den ZigBee-Funk", + "description": "W\u00e4hle die serielle Schnittstelle f\u00fcr den ZigBee-Funk", "title": "ZHA" } } @@ -44,7 +44,7 @@ "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", - "enable_identify_on_join": "Aktivieren Sie den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", + "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index aaa563ffddf..4198352aae8 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -33,6 +33,20 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kode diperlukan untuk tindakan pengaktifkan alarm", + "alarm_failed_tries": "Jumlah entri kode yang gagal berturut-turut untuk memicu alarm", + "alarm_master_code": "Kode master untuk panel kontrol alarm", + "title": "Opsi Panel Kontrol Alarm" + }, + "zha_options": { + "consider_unavailable_battery": "Anggap perangkat bertenaga baterai sebagai tidak tersedia setelah (detik)", + "consider_unavailable_mains": "Anggap perangkat bertenaga listrik sebagai tidak tersedia setelah (detik)", + "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", + "title": "Opsi Global" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 046cdd59485..61ea6762c7d 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.", + "addon_info_failed": "Gagal mendapatkan info add-on Z-Wave JS.", + "addon_install_failed": "Gagal menginstal add-on Z-Wave JS.", + "addon_set_config_failed": "Gagal menyetel konfigurasi Z-Wave JS.", + "addon_start_failed": "Gagal memulai add-on Z-Wave JS.", + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "different_device": "Perangkat USB yang terhubung tidak sama dengan yang dikonfigurasi sebelumnya untuk entri konfigurasi ini. Buat entri konfigurasi baru untuk perangkat baru." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_ws_url": "URL websocket tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "progress": { + "install_addon": "Harap tunggu hingga penginstalan add-on Z-Wave JS selesai. Ini bisa memakan waktu beberapa saat.", + "start_addon": "Harap tunggu hingga add-on Z-Wave JS selesai. Ini mungkin perlu waktu beberapa saat." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulasikan Perangkat Keras", + "log_level": "Tingkat log", + "network_key": "Kunci Jaringan", + "usb_path": "Jalur Perangkat USB" + }, + "title": "Masukkan konfigurasi add-on Z-Wave JS" + }, + "install_addon": { + "title": "Instalasi add-on Z-Wave JS telah dimulai" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Gunakan add-on Supervisor Z-Wave JS" + }, + "description": "Ingin menggunakan add-on Supervisor Z-Wave JS?", + "title": "Pilih metode koneksi" + }, + "start_addon": { + "title": "Add-on Z-Wave JS sedang dimulai." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file From fb7b2022519a45f659a831d43f1ba1ba9bf38543 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 9 Jul 2021 22:37:56 -0400 Subject: [PATCH 183/818] Bump up ZHA depdencies (#52818) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d37abea2310..081941d94fe 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,7 +10,7 @@ "zha-quirks==0.0.59", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.35.1", + "zigpy==0.35.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.1" diff --git a/requirements_all.txt b/requirements_all.txt index 085c7584dd5..d223bfea457 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2450,7 +2450,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.35.1 +zigpy==0.35.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47f01a0b962..ee6aa222279 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1344,7 +1344,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.35.1 +zigpy==0.35.2 # homeassistant.components.zwave_js zwave-js-server-python==0.27.0 From dd648f5c9c4a576c27adb8f1964c6a346f96982f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Jul 2021 22:31:42 +0200 Subject: [PATCH 184/818] Update arcam lib to 0.7.0 (#52829) --- homeassistant/components/arcam_fmj/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index d38ceceba73..6685ea240eb 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,7 +3,7 @@ "name": "Arcam FMJ Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.5.3"], + "requirements": ["arcam-fmj==0.7.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/requirements_all.txt b/requirements_all.txt index d223bfea457..ccd04eaee80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -290,7 +290,7 @@ aprslib==0.6.46 aqualogic==2.6 # homeassistant.components.arcam_fmj -arcam-fmj==0.5.3 +arcam-fmj==0.7.0 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee6aa222279..810a6b7aa1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -191,7 +191,7 @@ apprise==0.9.3 aprslib==0.6.46 # homeassistant.components.arcam_fmj -arcam-fmj==0.5.3 +arcam-fmj==0.7.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp From b5cec353cc624c5c8a694f80b6cd976d44c3034e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 Jul 2021 22:58:37 +0200 Subject: [PATCH 185/818] Fix pylint issue with stream component c-extension (#52847) * Rename 'extension-pkg-whitelist' setting to 'extension-pkg-allow-list' * Add 'av.stream' and 'av.audio.stream' * Replace 'Any' type hint --- homeassistant/components/stream/worker.py | 4 ++-- pyproject.toml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index ed1e1b9551d..c4ae3f30e18 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -45,9 +45,9 @@ class SegmentBuffer: self._memory_file: BytesIO = cast(BytesIO, None) self._av_output: av.container.OutputContainer = None self._input_video_stream: av.video.VideoStream = None - self._input_audio_stream: Any | None = None # av.audio.AudioStream | None + self._input_audio_stream: av.audio.stream.AudioStream | None = None self._output_video_stream: av.video.VideoStream = None - self._output_audio_stream: Any | None = None # av.audio.AudioStream | None + self._output_audio_stream: av.audio.stream.AudioStream | None = None self._segment: Segment | None = None # the following 3 member variables are used for Part formation self._memory_file_pos: int = cast(int, None) diff --git a/pyproject.toml b/pyproject.toml index f8d47624c8f..ee6e0015a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,9 @@ load-plugins = [ "hass_logger", ] persistent = false -extension-pkg-whitelist = [ +extension-pkg-allow-list = [ + "av.audio.stream", + "av.stream", "ciso8601", "cv2", ] From b484969b09322f09f712b974d0e382a6cd0d3556 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jul 2021 00:09:40 +0000 Subject: [PATCH 186/818] [ci skip] Translation update --- .../components/advantage_air/translations/de.json | 2 +- homeassistant/components/asuswrt/translations/de.json | 2 +- homeassistant/components/bosch_shc/translations/de.json | 2 +- homeassistant/components/braviatv/translations/de.json | 2 +- homeassistant/components/brother/translations/de.json | 2 +- homeassistant/components/coinbase/translations/de.json | 2 +- homeassistant/components/flume/translations/de.json | 4 ++-- homeassistant/components/flunearyou/translations/de.json | 4 ++-- .../components/forecast_solar/translations/de.json | 4 ++-- homeassistant/components/freebox/translations/de.json | 2 +- homeassistant/components/fritz/translations/de.json | 4 ++-- .../components/garmin_connect/translations/de.json | 2 +- homeassistant/components/goalzero/translations/de.json | 2 +- .../components/homekit_controller/translations/de.json | 2 +- homeassistant/components/icloud/translations/de.json | 2 +- homeassistant/components/ipp/translations/de.json | 4 ++-- homeassistant/components/konnected/translations/de.json | 8 ++++---- homeassistant/components/melcloud/translations/de.json | 2 +- .../components/meteo_france/translations/de.json | 2 +- .../components/modern_forms/translations/de.json | 2 +- homeassistant/components/nightscout/translations/de.json | 2 +- homeassistant/components/nuheat/translations/de.json | 2 +- homeassistant/components/nut/translations/de.json | 6 +++--- homeassistant/components/onvif/translations/de.json | 2 +- homeassistant/components/owntracks/translations/de.json | 2 +- homeassistant/components/plaato/translations/de.json | 2 +- homeassistant/components/ps4/translations/de.json | 2 +- homeassistant/components/roomba/translations/de.json | 4 ++-- homeassistant/components/shelly/translations/de.json | 2 +- homeassistant/components/smartthings/translations/de.json | 6 +++--- homeassistant/components/solarlog/translations/de.json | 2 +- homeassistant/components/spotify/translations/de.json | 2 +- homeassistant/components/subaru/translations/de.json | 2 +- .../components/synology_dsm/translations/de.json | 2 +- homeassistant/components/tuya/translations/de.json | 4 ++-- .../components/twentemilieu/translations/de.json | 2 +- homeassistant/components/twinkly/translations/de.json | 4 ++-- homeassistant/components/vera/translations/de.json | 6 +++--- homeassistant/components/vilfo/translations/de.json | 2 +- homeassistant/components/vizio/translations/de.json | 2 +- homeassistant/components/wled/translations/de.json | 4 ++-- 41 files changed, 59 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index c761ac5c6be..d3eb0296847 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -12,7 +12,7 @@ "ip_address": "IP Adresse", "port": "Port" }, - "description": "Anschluss an die API Ihres Advantage Air Wandtabletts.", + "description": "Anschluss an die API deines Advantage Air Wandtabletts.", "title": "Verbinden" } } diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index bf7e2230810..fcd2157d321 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -23,7 +23,7 @@ "ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)", "username": "Benutzername" }, - "description": "Einstellen der erforderlichen Parameter f\u00fcr die Verbindung mit Ihrem Router.", + "description": "Einstellen der erforderlichen Parameter f\u00fcr die Verbindung mit deinem Router.", "title": "" } } diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index c99af84b8b5..46b6469afe6 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -22,7 +22,7 @@ } }, "reauth_confirm": { - "description": "Die bosch_shc-Integration muss Ihr Konto neu authentifizieren", + "description": "Die bosch_shc-Integration muss dein Konto neu authentifizieren", "title": "Integration erneut authentifizieren" }, "user": { diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index dd1eadb630a..cca5c5aa47f 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", - "unsupported_model": "Ihr TV-Modell wird nicht unterst\u00fctzt." + "unsupported_model": "Dein TV-Modell wird nicht unterst\u00fctzt." }, "step": { "authorize": { diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 3390ca6ca8f..7b9f811ac32 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -22,7 +22,7 @@ "data": { "type": "Typ des Druckers" }, - "description": "M\u00f6chten Sie den Brother Drucker {model} mit der Seriennummer `{serial_number}` zum Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du den Brother Drucker {model} mit der Seriennummer `{serial_number}` zum Home Assistant hinzuf\u00fcgen?", "title": "Brother-Drucker entdeckt" } } diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 60195c794a1..e1fb1fdbcad 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -23,7 +23,7 @@ }, "options": { "error": { - "currency_unavaliable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von Ihrer Coinbase-API nicht bereitgestellt.", + "currency_unavaliable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von deiner Coinbase-API nicht bereitgestellt.", "exchange_rate_unavaliable": "Einer oder mehrere der angeforderten Wechselkurse werden nicht von Coinbase bereitgestellt.", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index f40fb92edcd..574763f24cc 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -24,8 +24,8 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Um auf die Flume Personal API zugreifen zu k\u00f6nnen, m\u00fcssen Sie unter https://portal.flumetech.com/settings#token eine 'Client ID' und 'Client Secret' anfordern", - "title": "Stellen Sie eine Verbindung zu Ihrem Flume-Konto her" + "description": "Um auf die Flume Personal API zugreifen zu k\u00f6nnen, musst du unter https://portal.flumetech.com/settings#token eine 'Client ID' und 'Client Secret' anfordern", + "title": "Stelle eine Verbindung zu deinem Flume-Konto her" } } } diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index 585923fb2bf..61dd2cd4ce7 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -12,8 +12,8 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, - "description": "\u00dcberwachen Sie benutzerbasierte und CDC-Berichte f\u00fcr ein Koordinatenpaar.", - "title": "Konfigurieren Sie die Grippe in Ihrer N\u00e4he" + "description": "\u00dcberwache benutzerbasierte und CDC-Berichte f\u00fcr ein Koordinatenpaar.", + "title": "Konfiguriere Grippe in deiner N\u00e4he" } } } diff --git a/homeassistant/components/forecast_solar/translations/de.json b/homeassistant/components/forecast_solar/translations/de.json index 86b51a14845..43b60424cf1 100644 --- a/homeassistant/components/forecast_solar/translations/de.json +++ b/homeassistant/components/forecast_solar/translations/de.json @@ -7,7 +7,7 @@ "declination": "Deklination (0 = Horizontal, 90 = Vertikal)", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", - "modules power": "Gesamt-Watt-Spitzenleistung Ihrer Solarmodule", + "modules power": "Gesamt-Watt-Spitzenleistung deiner Solarmodule", "name": "Name" }, "description": "Gib die Daten deiner Solarmodule ein. Wenn ein Feld unklar ist, schlage bitte in der Dokumentation nach." @@ -22,7 +22,7 @@ "azimuth": "Azimut (360 Grad, 0 = Norden, 90 = Osten, 180 = S\u00fcden, 270 = Westen)", "damping": "D\u00e4mpfungsfaktor: passt die Ergebnisse morgens und abends an", "declination": "Deklination (0 = Horizontal, 90 = Vertikal)", - "modules power": "Gesamt-Watt-Spitzenleistung Ihrer Solarmodule" + "modules power": "Gesamt-Watt-Spitzenleistung deiner Solarmodule" }, "description": "Mit diesen Werten kann das Solar.Forecast-Ergebnis angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." } diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json index 738b9d48f3c..50644a87982 100644 --- a/homeassistant/components/freebox/translations/de.json +++ b/homeassistant/components/freebox/translations/de.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Klicken Sie auf \"Senden\" und ber\u00fchren Sie dann den Pfeil nach rechts auf dem Router, um Freebox bei Home Assistant zu registrieren. \n\n ![Position der Schaltfl\u00e4che am Router]\n (/static/images/config_freebox.png)", + "description": "Klicke auf \"Senden\" und ber\u00fchre dann den Pfeil nach rechts auf dem Router, um Freebox bei Home Assistant zu registrieren. \n\n ![Position der Schaltfl\u00e4che am Router](/static/images/config_freebox.png)", "title": "Link Freebox Router" }, "user": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index dcded6750e9..47938084f5b 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -37,7 +37,7 @@ "port": "Port", "username": "Benutzername" }, - "description": "Einrichten der FRITZ!Box Tools zur Steuerung Ihrer FRITZ!Box.\n Ben\u00f6tigt: Benutzername, Passwort.", + "description": "Einrichten der FRITZ!Box Tools zur Steuerung deiner FRITZ!Box.\nBen\u00f6tigt: Benutzername, Passwort.", "title": "Setup FRITZ!Box Tools - obligatorisch" }, "user": { @@ -47,7 +47,7 @@ "port": "Port", "username": "Benutzername" }, - "description": "FRITZ!Box Tools einrichten, um Ihre FRITZ!Box zu steuern.\nMindestens erforderlich: Benutzername, Passwort.", + "description": "FRITZ!Box Tools einrichten, um deine FRITZ!Box zu steuern.\nMindestens erforderlich: Benutzername, Passwort.", "title": "Setup FRITZ!Box Tools" } } diff --git a/homeassistant/components/garmin_connect/translations/de.json b/homeassistant/components/garmin_connect/translations/de.json index 9186f753a77..7817a44f6c0 100644 --- a/homeassistant/components/garmin_connect/translations/de.json +++ b/homeassistant/components/garmin_connect/translations/de.json @@ -15,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Geben Sie Ihre Zugangsdaten ein.", + "description": "Gib deine Zugangsdaten ein.", "title": "Garmin Connect" } } diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 3049a4f77ef..d483564fa72 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -12,7 +12,7 @@ }, "step": { "confirm_discovery": { - "description": "Eine DHCP-Reservierung auf Ihrem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage im Benutzerhandbuch deines Routers nach.", + "description": "Eine DHCP-Reservierung auf deinem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage im Benutzerhandbuch deines Routers nach.", "title": "Goal Zero Yeti" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 1fac267a0ad..248d3871b3e 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Pairing mit unsicheren Setup-Codes zulassen.", "pairing_code": "Kopplungscode" }, - "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Geben Sie Ihren HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.", + "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "protocol_error": { diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 431b9f35da2..4cc7ed93eef 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Konto wurde bereits konfiguriert", - "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert", + "no_device": "Auf keinem deiner Ger\u00e4te ist \"Find my iPhone\" aktiviert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 289e75af55d..9886bf9c0ef 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -23,8 +23,8 @@ "ssl": "Verwendet ein SSL-Zertifikat", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "description": "Richten Sie Ihren Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.", - "title": "Verbinden Sie Ihren Drucker" + "description": "Richte deinen Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.", + "title": "Verbinde deinen Drucker" }, "zeroconf_confirm": { "description": "M\u00f6chtest du {name} einrichten?", diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index a438767a3f1..fd307e90f20 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -66,7 +66,7 @@ "7": "Zone 7", "out": "OUT" }, - "description": "Es wurde ein {model} bei {host} entdeckt. W\u00e4hlen Sie unten die Basiskonfiguration der einzelnen E / A aus. Je nach E / A k\u00f6nnen bin\u00e4re Sensoren (Kontakte \u00f6ffnen / schlie\u00dfen), digitale Sensoren (dht und ds18b20) oder umschaltbare Ausg\u00e4nge verwendet werden. In den n\u00e4chsten Schritten k\u00f6nnen Sie detaillierte Optionen konfigurieren.", + "description": "Es wurde ein {model} bei {host} entdeckt. W\u00e4hle unten die Basiskonfiguration der einzelnen E / A aus. Je nach E / A k\u00f6nnen bin\u00e4re Sensoren (Kontakte \u00f6ffnen / schlie\u00dfen), digitale Sensoren (dht und ds18b20) oder umschaltbare Ausg\u00e4nge verwendet werden. In den n\u00e4chsten Schritten kannst du detaillierte Optionen konfigurieren.", "title": "Konfigurieren von I/O" }, "options_io_ext": { @@ -80,15 +80,15 @@ "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" }, - "description": "W\u00e4hlen Sie unten die Konfiguration der verbleibenden E / A. In den n\u00e4chsten Schritten k\u00f6nnen Sie detaillierte Optionen konfigurieren.", + "description": "W\u00e4hle unten die Konfiguration der verbleibenden E / A. In den n\u00e4chsten Schritten kannst du detaillierte Optionen konfigurieren.", "title": "Konfiguriere Erweiterte I/O" }, "options_misc": { "data": { "api_host": "API-Host-URL \u00fcberschreiben (optional)", "blink": "LED Panel blinkt beim senden von Status\u00e4nderungen", - "discovery": "Reagieren auf Suchanfragen in Ihrem Netzwerk", - "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" + "discovery": "Reagieren auf Suchanfragen in deinem Netzwerk", + "override_api_host": "\u00dcberschreibe die Standard-Host-Panel-URL der Home Assistant-API" }, "description": "Bitte w\u00e4hle das gew\u00fcnschte Verhalten f\u00fcr dein Panel", "title": "Sonstiges konfigurieren" diff --git a/homeassistant/components/melcloud/translations/de.json b/homeassistant/components/melcloud/translations/de.json index 7e4825587c7..a0d6ce662ba 100644 --- a/homeassistant/components/melcloud/translations/de.json +++ b/homeassistant/components/melcloud/translations/de.json @@ -14,7 +14,7 @@ "password": "Passwort", "username": "E-Mail-Adresse" }, - "description": "Verbinden Sie sich mit Ihrem MELCloud-Konto.", + "description": "Verbinde dich mit deinem MELCloud-Konto.", "title": "Stelle eine Verbindung zu MELCloud her" } } diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json index e1993b466dc..04d038cb65d 100644 --- a/homeassistant/components/meteo_france/translations/de.json +++ b/homeassistant/components/meteo_france/translations/de.json @@ -19,7 +19,7 @@ "data": { "city": "Stadt" }, - "description": "Geben Sie die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein", + "description": "Gib die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein", "title": "M\u00e9t\u00e9o-France" } } diff --git a/homeassistant/components/modern_forms/translations/de.json b/homeassistant/components/modern_forms/translations/de.json index 9c8e1b21518..bf16c9532fc 100644 --- a/homeassistant/components/modern_forms/translations/de.json +++ b/homeassistant/components/modern_forms/translations/de.json @@ -16,7 +16,7 @@ "data": { "host": "Host" }, - "description": "Einrichten Ihres Modern Forms Ventilator f\u00fcr die Integration in Home Assistant." + "description": "Einrichten deines Modern Forms Ventilator f\u00fcr die Integration in Home Assistant." }, "zeroconf_confirm": { "description": "M\u00f6chtest du den Modern Forms Ventilator mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index 02a301b1e78..21bea5ee877 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -15,7 +15,7 @@ "api_key": "API-Schl\u00fcssel", "url": "URL" }, - "description": "- URL: die Adresse Ihrer Nightscout-Instanz. Z.B.: https://myhomeassistant.duckdns.org:5423\n- API-Schl\u00fcssel (Optional): Nur verwenden, wenn Ihre Instanz gesch\u00fctzt ist (auth_default_roles != readable).", + "description": "- URL: die Adresse deiner Nightscout-Instanz. Z.B.: https://myhomeassistant.duckdns.org:5423\n- API-Schl\u00fcssel (Optional): Nur verwenden, wenn deine Instanz gesch\u00fctzt ist (auth_default_roles != readable).", "title": "Gib deine Nightscout-Serverinformationen ein." } } diff --git a/homeassistant/components/nuheat/translations/de.json b/homeassistant/components/nuheat/translations/de.json index 47c1a90c459..0ab69dd4557 100644 --- a/homeassistant/components/nuheat/translations/de.json +++ b/homeassistant/components/nuheat/translations/de.json @@ -16,7 +16,7 @@ "serial_number": "Seriennummer des Thermostats.", "username": "Benutzername" }, - "description": "Du musst die numerische Seriennummer oder ID Ihres Thermostats erhalten, indem du dich bei https://MyNuHeat.com anmeldest und deine Thermostate ausw\u00e4hlst.", + "description": "Du musst die numerische Seriennummer oder ID deines Thermostats erhalten, indem du dich bei https://MyNuHeat.com anmeldest und deine Thermostate ausw\u00e4hlst.", "title": "Stelle eine Verbindung zu NuHeat her" } } diff --git a/homeassistant/components/nut/translations/de.json b/homeassistant/components/nut/translations/de.json index 990f21523b6..e82dca2506b 100644 --- a/homeassistant/components/nut/translations/de.json +++ b/homeassistant/components/nut/translations/de.json @@ -12,7 +12,7 @@ "data": { "resources": "Ressourcen" }, - "title": "W\u00e4hlen Sie die zu \u00fcberwachenden Ressourcen aus" + "title": "W\u00e4hle die zu \u00fcberwachenden Ressourcen aus" }, "ups": { "data": { @@ -28,7 +28,7 @@ "port": "Port", "username": "Benutzername" }, - "title": "Stellen Sie eine Verbindung zum NUT-Server her" + "title": "Stelle eine Verbindung zum NUT-Server her" } } }, @@ -43,7 +43,7 @@ "resources": "Ressourcen", "scan_interval": "Scan-Intervall (Sekunden)" }, - "description": "W\u00e4hlen Sie Sensorressourcen." + "description": "W\u00e4hle Sensorressourcen." } } } diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 4447b8dafea..fd992c4db05 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -53,7 +53,7 @@ "data": { "auto": "Automatisch suchen" }, - "description": "Wenn du auf Senden klickst, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stelle sicher, dass ONVIF in der Konfiguration deiner Kamera aktiviert ist.", + "description": "Wenn du auf Senden klickst, durchsuchen wir dein Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stelle sicher, dass ONVIF in der Konfiguration deiner Kamera aktiviert ist.", "title": "ONVIF-Ger\u00e4tekonfiguration" } } diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index da313efbe6e..891f914f8a9 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "Unter Android \u00f6ffnen Sie [die OwnTracks App]({android_url}), gehen Sie zu Einstellungen -> Verbindung. \u00c4ndern Sie die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffnen Sie [die OwnTracks App]({ios_url}), tippen Sie auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndern Sie die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen finden Sie in [der Dokumentation]({docs_url}).\n\n\u00dcbersetzt mit www.DeepL.com/Translator (kostenlose Version)" + "default": "Unter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen -> Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index ea079631e1c..29e3ebe9790 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, "create_entry": { - "default": "Ihr Plaato {device_type} mit dem Namen **{device_name}** wurde erfolgreich eingerichtet!" + "default": "Dein Plaato {device_type} mit dem Namen **{device_name}** wurde erfolgreich eingerichtet!" }, "error": { "invalid_webhook_device": "Du hast ein Ger\u00e4t gew\u00e4hlt, das das Senden von Daten an einen Webhook nicht unterst\u00fctzt. Es ist nur f\u00fcr die Airlock verf\u00fcgbar", diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 1a20740dfb1..59a84e7ad27 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", + "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index eb907115188..bfc98069881 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -34,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "Es wurde kein Roomba oder Braava in Ihrem Netzwerk entdeckt. Die BLID ist der Teil des Ger\u00e4te-Hostnamens nach `iRobot-` oder `Roomba-`. Bitte folge den Schritten in der Dokumentation unter: {auth_help_url}", + "description": "Es wurde kein Roomba oder Braava in deinem Netzwerk entdeckt. Die BLID ist der Teil des Ger\u00e4te-Hostnamens nach `iRobot-` oder `Roomba-`. Bitte folge den Schritten in der Dokumentation unter: {auth_help_url}", "title": "Manuell mit dem Ger\u00e4t verbinden" }, "user": { @@ -45,7 +45,7 @@ "host": "Host", "password": "Passwort" }, - "description": "W\u00e4hlen Sie einen Roomba oder Braava aus.", + "description": "W\u00e4hle einen Roomba oder Braava aus.", "title": "Automatisch mit dem Ger\u00e4t verbinden" } } diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 7e25744420e..513ff66dff1 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -12,7 +12,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "M\u00f6chtest du das {model} bei {host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Du kannst das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." + "description": "M\u00f6chtest du das {model} bei {host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor du mit dem Einrichten fortf\u00e4hrst.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Du kannst das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." }, "credentials": { "data": { diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index 21fd056982e..cd946ca8261 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n > {webhook_url} \n\n Bitte aktualisieren Sie Ihre Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starten Sie den Home Assistant neu und versuchen Sie es erneut.", + "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n > {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", "no_available_locations": "In Home Assistant sind keine SmartThings-Standorte zum Einrichten verf\u00fcgbar." }, "error": { @@ -9,7 +9,7 @@ "token_forbidden": "Das Token verf\u00fcgt nicht \u00fcber die erforderlichen OAuth-Bereiche.", "token_invalid_format": "Das Token muss im UID/GUID-Format vorliegen.", "token_unauthorized": "Das Token ist ung\u00fcltig oder nicht mehr autorisiert.", - "webhook_error": "SmartThings konnte die Webhook-URL nicht \u00fcberpr\u00fcfen. Bitte stellen Sie sicher, dass die Webhook-URL \u00fcber das Internet erreichbar ist, und versuchen Sie es erneut." + "webhook_error": "SmartThings konnte die Webhook-URL nicht \u00fcberpr\u00fcfen. Bitte stelle sicher, dass die Webhook-URL \u00fcber das Internet erreichbar ist, und versuche es erneut." }, "step": { "authorize": { @@ -30,7 +30,7 @@ "title": "Standort ausw\u00e4hlen" }, "user": { - "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n > {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisieren Sie bitte Ihre Konfiguration, starten Sie Home Assistant neu und versuchen Sie es erneut.", + "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n > {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", "title": "R\u00fcckruf-URL best\u00e4tigen" } } diff --git a/homeassistant/components/solarlog/translations/de.json b/homeassistant/components/solarlog/translations/de.json index 008e1058681..c2e38dcd940 100644 --- a/homeassistant/components/solarlog/translations/de.json +++ b/homeassistant/components/solarlog/translations/de.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "Host", - "name": "Das Pr\u00e4fix, das f\u00fcr Ihre Solar-Log-Sensoren verwendet werden soll" + "name": "Das Pr\u00e4fix, das f\u00fcr deine Solar-Log-Sensoren verwendet werden soll" }, "title": "Definiere deine Solar-Log-Verbindung" } diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 69984b39798..799f3717b81 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "reauth_account_mismatch": "Das Spotify-Konto, mit dem du dich authentifiziert hast, stimmt nicht mit dem Konto \u00fcberein, f\u00fcr das du dich erneut authentifizieren musst." }, diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index b1e55c3ade5..4f10a4266ae 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -35,7 +35,7 @@ "data": { "update_enabled": "Aktiviere die Fahrzeugabfrage" }, - "description": "Wenn diese Option aktiviert ist, sendet die Fahrzeugabfrage alle 2 Stunden einen Fernbefehl an Ihr Fahrzeug, um neue Sensordaten zu erhalten. Ohne Fahrzeugabfrage werden neue Sensordaten nur empfangen, wenn das Fahrzeug automatisch Daten sendet (normalerweise nach dem Abstellen des Motors).", + "description": "Wenn diese Option aktiviert ist, sendet die Fahrzeugabfrage alle 2 Stunden einen Fernbefehl an dein Fahrzeug, um neue Sensordaten zu erhalten. Ohne Fahrzeugabfrage werden neue Sensordaten nur empfangen, wenn das Fahrzeug automatisch Daten sendet (normalerweise nach dem Abstellen des Motors).", "title": "Subaru Starlink Optionen" } } diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 766fc7f6e91..74867aa9044 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -26,7 +26,7 @@ "username": "Benutzername", "verify_ssl": "SSL Zertifikat verifizieren" }, - "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "description": "M\u00f6chtest du {name} ({host}) einrichten?", "title": "Synology DSM" }, "user": { diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index ac5a7e77b19..54fd3de7cbf 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "country_code": "L\u00e4ndercode Ihres Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", + "country_code": "L\u00e4ndercode deines Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", "password": "Passwort", - "platform": "Die App, in der Ihr Konto registriert ist", + "platform": "Die App, in der dein Konto registriert ist", "username": "Benutzername" }, "description": "Gib deine Tuya-Anmeldeinformationen ein.", diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 38cabb6c22e..4ce9ed23ea7 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -14,7 +14,7 @@ "house_number": "Hausnummer", "post_code": "Postleitzahl" }, - "description": "Richte Twente Milieu mit Informationen zur Abfallsammlung unter Ihrer Adresse ein.", + "description": "Richte Twente Milieu mit Informationen zur Abfallsammlung unter deiner Adresse ein.", "title": "Twente Milieu" } } diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index 2e8bf218e08..0f5a7e0b886 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "host": "Host (oder IP-Adresse) Ihres twinkly-Ger\u00e4ts" + "host": "Host (oder IP-Adresse) deines twinkly-Ger\u00e4ts" }, - "description": "Einrichten Ihrer Twinkly-Led-Kette", + "description": "Einrichten deiner Twinkly-Led-Kette", "title": "Twinkly" } } diff --git a/homeassistant/components/vera/translations/de.json b/homeassistant/components/vera/translations/de.json index 7fce8f6bdfe..999dfc8213f 100644 --- a/homeassistant/components/vera/translations/de.json +++ b/homeassistant/components/vera/translations/de.json @@ -10,8 +10,8 @@ "lights": "Vera Switch-Ger\u00e4te-IDs, die im Home Assistant als Lichter behandelt werden sollen.", "vera_controller_url": "Controller-URL" }, - "description": "Stellen Sie unten eine Vera-Controller-URL zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.", - "title": "Richten Sie den Vera-Controller ein" + "description": "Stelle unten eine Vera-Controller-URL zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.", + "title": "Richte den Vera-Controller ein" } } }, @@ -22,7 +22,7 @@ "exclude": "Vera-Ger\u00e4te-IDs, die vom Home Assistant ausgeschlossen werden sollen.", "lights": "Vera Switch-Ger\u00e4te-IDs, die im Home Assistant als Lichter behandelt werden sollen." }, - "description": "Weitere Informationen zu optionalen Parametern finden Sie in der Vera-Dokumentation: https://www.home-assistant.io/integrations/vera/. Hinweis: Alle \u00c4nderungen hier erfordern einen Neustart des Home Assistant-Servers. Geben Sie ein Leerzeichen ein, um Werte zu l\u00f6schen.", + "description": "Weitere Informationen zu optionalen Parametern findest du in der Vera-Dokumentation: https://www.home-assistant.io/integrations/vera/. Hinweis: Alle \u00c4nderungen hier erfordern einen Neustart des Home Assistant-Servers. Gib ein Leerzeichen ein, um Werte zu l\u00f6schen.", "title": "Vera Controller Optionen" } } diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json index 01c4a894c24..0e146a4159f 100644 --- a/homeassistant/components/vilfo/translations/de.json +++ b/homeassistant/components/vilfo/translations/de.json @@ -14,7 +14,7 @@ "access_token": "Zugriffstoken", "host": "Host" }, - "description": "Richten Sie die Vilfo Router-Integration ein. Sie ben\u00f6tigen Ihren Vilfo Router-Hostnamen / Ihre IP-Adresse und ein API-Zugriffstoken. Weitere Informationen zu dieser Integration und wie Sie diese Details erhalten, finden Sie unter: https://www.home-assistant.io/integrations/vilfo", + "description": "Richte die Vilfo Router-Integration ein. Du ben\u00f6tigst deinen Vilfo Router-Hostnamen / deine IP-Adresse und ein API-Zugriffstoken. Weitere Informationen zu dieser Integration und wie du diese Details erh\u00e4ltst, findest du unter: https://www.home-assistant.io/integrations/vilfo", "title": "Stelle eine Verbindung zum Vilfo Router her" } } diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index 0ed6fac9e83..a47c2e0f036 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -46,7 +46,7 @@ "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?", "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, - "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", + "description": "Wenn du \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgst, kannst du deine Quellliste optional filtern, indem du ausw\u00e4hlst, welche Apps in deine Quellliste aufgenommen oder ausgeschlossen werden sollen.", "title": "Aktualisiere die VIZIO SmartCast-Ger\u00e4t-Optionen" } } diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index d03ef92d041..e97fb86d3e8 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -13,10 +13,10 @@ "data": { "host": "Host" }, - "description": "Richten Sie Ihren WLED f\u00fcr die Integration mit Home Assistant ein." + "description": "Richte deinen WLED f\u00fcr die Integration mit Home Assistant ein." }, "zeroconf_confirm": { - "description": "M\u00f6chten Sie die WLED mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du die WLED mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", "title": "WLED-Ger\u00e4t entdeckt" } } From 2317b7343ff00b33d467d9e8c31ef4e36ec46de5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Jul 2021 10:14:50 -0400 Subject: [PATCH 187/818] Rename preview task to run (#52857) --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0226b3f4361..1308f535428 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "tasks": [ { - "label": "Preview", + "label": "Run Home Assistant Core", "type": "shell", "command": "hass -c ./config", "group": { From f234da63798061803a13fb23b64f8ce36e1713e4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Jul 2021 16:27:46 -0400 Subject: [PATCH 188/818] Bump zwave-js-server-python to 0.27.1 (#52885) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index a48d0513c17..d719e3976a4 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.27.0"], + "requirements": ["zwave-js-server-python==0.27.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index ccd04eaee80..6758a9e7a94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2456,4 +2456,4 @@ zigpy==0.35.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.27.0 +zwave-js-server-python==0.27.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 810a6b7aa1c..f8aa98359a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,4 +1347,4 @@ zigpy-znp==0.5.1 zigpy==0.35.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.27.0 +zwave-js-server-python==0.27.1 From d7b2ec80b2c5c90ec961a30dffe835cf23d8d59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 11 Jul 2021 22:33:03 +0200 Subject: [PATCH 189/818] Bump pyhaversion to 21.7.0 (#52880) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 6f36c337a76..de43a47d505 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ - "pyhaversion==21.5.0" + "pyhaversion==21.7.0" ], "codeowners": [ "@fabaff", diff --git a/requirements_all.txt b/requirements_all.txt index 6758a9e7a94..5773b2684f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1465,7 +1465,7 @@ pygtfs==0.1.6 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.5.0 +pyhaversion==21.7.0 # homeassistant.components.heos pyheos==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8aa98359a8..70a42e76a80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -818,7 +818,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.5.0 +pyhaversion==21.7.0 # homeassistant.components.heos pyheos==0.7.2 From d01227f141bad57d1252ec8380fe18453458f529 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 11 Jul 2021 16:35:36 -0400 Subject: [PATCH 190/818] Use entity class attributes for bbb_gpio (#52837) --- .../components/bbb_gpio/binary_sensor.py | 16 ++++------------ homeassistant/components/bbb_gpio/switch.py | 16 ++++------------ 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py index c772cf86f00..5fcaccd674e 100644 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -43,10 +43,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BBBGPIOBinarySensor(BinarySensorEntity): """Representation of a binary sensor that uses Beaglebone Black GPIO.""" + _attr_should_poll = False + def __init__(self, pin, params): """Initialize the Beaglebone Black binary sensor.""" self._pin = pin - self._name = params[CONF_NAME] or DEVICE_DEFAULT_NAME + self._attr_name = params[CONF_NAME] or DEVICE_DEFAULT_NAME self._bouncetime = params[CONF_BOUNCETIME] self._pull_mode = params[CONF_PULL_MODE] self._invert_logic = params[CONF_INVERT_LOGIC] @@ -62,16 +64,6 @@ class BBBGPIOBinarySensor(BinarySensorEntity): bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime) @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the entity.""" return self._state != self._invert_logic diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py index 03a9065a15b..3bed1d7db13 100644 --- a/homeassistant/components/bbb_gpio/switch.py +++ b/homeassistant/components/bbb_gpio/switch.py @@ -37,10 +37,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BBBGPIOSwitch(ToggleEntity): """Representation of a BeagleBone Black GPIO.""" + _attr_should_poll = False + def __init__(self, pin, params): """Initialize the pin.""" self._pin = pin - self._name = params[CONF_NAME] or DEVICE_DEFAULT_NAME + self._attr_name = params[CONF_NAME] or DEVICE_DEFAULT_NAME self._state = params[CONF_INITIAL] self._invert_logic = params[CONF_INVERT_LOGIC] @@ -52,17 +54,7 @@ class BBBGPIOSwitch(ToggleEntity): bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state From 77c68cb50744a596ddfb9d07859bb2fa76b8f2ce Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 11 Jul 2021 16:37:41 -0400 Subject: [PATCH 191/818] Use entity class attributes for bayesian (#52831) --- .../components/bayesian/binary_sensor.py | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 6879e278bab..41654973ffe 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -129,13 +129,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BayesianBinarySensor(BinarySensorEntity): """Representation of a Bayesian sensor.""" + _attr_should_poll = False + def __init__(self, name, prior, observations, probability_threshold, device_class): """Initialize the Bayesian sensor.""" - self._name = name + self._attr_name = name self._observations = observations self._probability_threshold = probability_threshold - self._device_class = device_class - self._deviation = False + self._attr_device_class = device_class + self._attr_is_on = False self._callbacks = [] self.prior = prior @@ -238,12 +240,12 @@ class BayesianBinarySensor(BinarySensorEntity): self.current_observations.update(self._initialize_current_observations()) self.probability = self._calculate_new_probability() - self._deviation = bool(self.probability >= self._probability_threshold) + self._attr_is_on = bool(self.probability >= self._probability_threshold) @callback def _recalculate_and_write_state(self): self.probability = self._calculate_new_probability() - self._deviation = bool(self.probability >= self._probability_threshold) + self._attr_is_on = bool(self.probability >= self._probability_threshold) self.async_write_ha_state() def _initialize_current_observations(self): @@ -363,30 +365,9 @@ class BayesianBinarySensor(BinarySensorEntity): except ConditionError: return False - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._deviation - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_class(self): - """Return the sensor class of the sensor.""" - return self._device_class - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" - attr_observations_list = [ obs.copy() for obs in self.current_observations.values() if obs is not None ] From 5849a97150705087be91a1e0ba1f1763a0eaf619 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 11 Jul 2021 16:41:14 -0400 Subject: [PATCH 192/818] Use entity class attributes for Beewi smartclim (#52839) * Use entity class attributes for beewi_smartclim * rework --- .../components/beewi_smartclim/sensor.py | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 9bf935f3c4f..2ed6b71be41 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -61,44 +61,19 @@ class BeewiSmartclimSensor(SensorEntity): def __init__(self, poller, name, mac, device, unit): """Initialize the sensor.""" self._poller = poller - self._name = name - self._mac = mac + self._attr_name = name self._device = device - self._unit = unit - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor. State is returned in Celsius.""" - return self._state - - @property - def device_class(self): - """Device class of this entity.""" - return self._device - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._mac}_{self._device}" - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit + self._attr_unit_of_measurement = unit + self._attr_device_class = self._device + self._attr_unique_id = f"{mac}_{device}" def update(self): """Fetch new state data from the poller.""" self._poller.update_sensor() - self._state = None + self._attr_state = None if self._device == DEVICE_CLASS_TEMPERATURE: - self._state = self._poller.get_temperature() + self._attr_state = self._poller.get_temperature() if self._device == DEVICE_CLASS_HUMIDITY: - self._state = self._poller.get_humidity() + self._attr_state = self._poller.get_humidity() if self._device == DEVICE_CLASS_BATTERY: - self._state = self._poller.get_battery() + self._attr_state = self._poller.get_battery() From 9b577e830d4b2ebf3eb80deae94e53322fdc4cbf Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 11 Jul 2021 16:42:52 -0400 Subject: [PATCH 193/818] Use entity class attributes for azure_devops (#52698) --- .../components/azure_devops/__init__.py | 26 +++----------- .../components/azure_devops/sensor.py | 36 ++++--------------- 2 files changed, 11 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 5e3971adcc4..f5537a63291 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -55,38 +55,22 @@ class AzureDevOpsEntity(Entity): def __init__(self, organization: str, project: str, name: str, icon: str) -> None: """Initialize the Azure DevOps entity.""" - self._name = name - self._icon = icon - self._available = True + self._attr_name = name + self._attr_icon = icon self.organization = organization self.project = project - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - async def async_update(self) -> None: """Update Azure DevOps entity.""" if await self._azure_devops_update(): - self._available = True + self._attr_available = True else: - if self._available: + if self._attr_available: _LOGGER.debug( "An error occurred while updating Azure DevOps sensor", exc_info=True, ) - self._available = False + self._attr_available = False async def _azure_devops_update(self) -> None: """Update Azure DevOps entity.""" diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 170cd244884..d7589cf5014 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -71,38 +71,14 @@ class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): unit_of_measurement: str = "", ) -> None: """Initialize Azure DevOps sensor.""" - self._state = None - self._attributes = None - self._available = False - self._unit_of_measurement = unit_of_measurement - self.measurement = measurement + self._attr_unit_of_measurement = unit_of_measurement self.client = client self.organization = organization self.project = project - self.key = key + self._attr_unique_id = "_".join([organization, key]) super().__init__(organization, project, name, icon) - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join([self.organization, self.key]) - - @property - def state(self) -> str: - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self) -> object: - """Return the attributes of the sensor.""" - return self._attributes - - @property - def unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): """Defines a Azure DevOps card count sensor.""" @@ -129,10 +105,10 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): ) except aiohttp.ClientError as exception: _LOGGER.warning(exception) - self._available = False + self._attr_available = False return False - self._state = build.build_number - self._attributes = { + self._attr_state = build.build_number + self._attr_extra_state_attributes = { "definition_id": build.definition.id, "definition_name": build.definition.name, "id": build.id, @@ -146,5 +122,5 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): "start_time": build.start_time, "finish_time": build.finish_time, } - self._available = True + self._attr_available = True return True From c865a1876e7d73ef851a3e74de4b5e81c2c287fb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 11 Jul 2021 16:45:05 -0400 Subject: [PATCH 194/818] Use entity class attributes for arlo (#52681) * Use entity class attributes for arlo * revert sensor --- .../components/arlo/alarm_control_panel.py | 51 +++++-------------- homeassistant/components/arlo/camera.py | 7 +-- 2 files changed, 15 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 91fb2a6a33e..b9a1004ac70 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -14,6 +14,7 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_DEVICE_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -33,8 +34,6 @@ CONF_HOME_MODE_NAME = "home_mode_name" CONF_AWAY_MODE_NAME = "away_mode_name" CONF_NIGHT_MODE_NAME = "night_mode_name" -DISARMED = "disarmed" - ICON = "mdi:security" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( @@ -69,18 +68,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ArloBaseStation(AlarmControlPanelEntity): """Representation of an Arlo Alarm Control Panel.""" + _attr_supported_features = ( + SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + ) + _attr_icon = ICON + def __init__(self, data, home_mode_name, away_mode_name, night_mode_name): """Initialize the alarm control panel.""" self._base_station = data self._home_mode_name = home_mode_name self._away_mode_name = away_mode_name self._night_mode_name = night_mode_name - self._state = None - - @property - def icon(self): - """Return icon.""" - return ICON + self._attr_name = data.name + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_DEVICE_ID: data.device_id, + } async def async_added_to_hass(self): """Register callbacks.""" @@ -95,28 +98,15 @@ class ArloBaseStation(AlarmControlPanelEntity): """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT - def update(self): """Update the state of the device.""" _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) mode = self._base_station.mode - if mode: - self._state = self._get_state_from_mode(mode) - else: - self._state = None + self._attr_state = self._get_state_from_mode(mode) if mode else None def alarm_disarm(self, code=None): """Send disarm command.""" - self._base_station.mode = DISARMED + self._base_station.mode = STATE_ALARM_DISARMED def alarm_arm_away(self, code=None): """Send arm away command. Uses custom mode.""" @@ -130,24 +120,11 @@ class ArloBaseStation(AlarmControlPanelEntity): """Send arm night command. Uses custom mode.""" self._base_station.mode = self._night_mode_name - @property - def name(self): - """Return the name of the base station.""" - return self._base_station.name - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "device_id": self._base_station.device_id, - } - def _get_state_from_mode(self, mode): """Convert Arlo mode to Home Assistant state.""" if mode == ARMED: return STATE_ALARM_ARMED_AWAY - if mode == DISARMED: + if mode == STATE_ALARM_DISARMED: return STATE_ALARM_DISARMED if mode == self._home_mode_name: return STATE_ALARM_ARMED_HOME diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index c1848661429..87c6216e56d 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -55,7 +55,7 @@ class ArloCam(Camera): """Initialize an Arlo camera.""" super().__init__() self._camera = camera - self._name = self._camera.name + self._attr_name = camera.name self._motion_status = False self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) @@ -102,11 +102,6 @@ class ArloCam(Camera): finally: await stream.close() - @property - def name(self): - """Return the name of this camera.""" - return self._name - @property def extra_state_attributes(self): """Return the state attributes.""" From 11d7efb785d210a8d8e9e197726b48fd126e6e9b Mon Sep 17 00:00:00 2001 From: EddyK69 Date: Sun, 11 Jul 2021 22:47:32 +0200 Subject: [PATCH 195/818] Add AllTrips sensors for BMW Connected Drive (#50420) * Add AllTrips sensors for BMW Connected Drive Added several new AllTrips sensors and some optional extra AllTrips sensors (disabled by default) * Fix for failed checks * Fix for failed check (black) * Code tidying Changed code after useful comments ;) --- .../components/bmw_connected_drive/sensor.py | 368 +++++++++++++++++- 1 file changed, 366 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 48d28e26f8a..053a5adff22 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,7 +1,7 @@ """Support for reading vehicle status from BMW connected drive portal.""" import logging -from bimmer_connected.const import SERVICE_LAST_TRIP, SERVICE_STATUS +from bimmer_connected.const import SERVICE_ALL_TRIPS, SERVICE_LAST_TRIP, SERVICE_STATUS from bimmer_connected.state import ChargingState from homeassistant.components.sensor import SensorEntity @@ -9,8 +9,10 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, LENGTH_KILOMETERS, LENGTH_MILES, + MASS_KILOGRAMS, PERCENTAGE, TIME_HOURS, TIME_MINUTES, @@ -60,6 +62,146 @@ ATTR_TO_HA_METRIC = { "electric_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], "saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], "total_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + # AllTrips attributes + "average_combined_consumption_community_average": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_combined_consumption_community_high": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_combined_consumption_community_low": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_combined_consumption_user_average": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "average_electric_consumption_community_average": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_electric_consumption_community_high": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_electric_consumption_community_low": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_electric_consumption_user_average": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "average_recuperation_community_average": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_recuperation_community_high": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_recuperation_community_low": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + False, + ], + "average_recuperation_user_average": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "chargecycle_range_community_average": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "chargecycle_range_community_high": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "chargecycle_range_community_low": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "chargecycle_range_user_average": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + True, + ], + "chargecycle_range_user_current_charge_cycle": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + True, + ], + "chargecycle_range_user_high": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + True, + ], + "total_electric_distance_community_average": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "total_electric_distance_community_high": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "total_electric_distance_community_low": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "total_electric_distance_user_average": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "total_electric_distance_user_total": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + False, + ], + "total_saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], } ATTR_TO_HA_IMPERIAL = { @@ -92,6 +234,146 @@ ATTR_TO_HA_IMPERIAL = { "electric_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], "saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], "total_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + # AllTrips attributes + "average_combined_consumption_community_average": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_combined_consumption_community_high": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_combined_consumption_community_low": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_combined_consumption_user_average": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "average_electric_consumption_community_average": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_electric_consumption_community_high": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_electric_consumption_community_low": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_electric_consumption_user_average": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "average_recuperation_community_average": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_recuperation_community_high": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_recuperation_community_low": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ], + "average_recuperation_user_average": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "chargecycle_range_community_average": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "chargecycle_range_community_high": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "chargecycle_range_community_low": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "chargecycle_range_user_average": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + True, + ], + "chargecycle_range_user_current_charge_cycle": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + True, + ], + "chargecycle_range_user_high": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + True, + ], + "total_electric_distance_community_average": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "total_electric_distance_community_high": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "total_electric_distance_community_low": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "total_electric_distance_user_average": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "total_electric_distance_user_total": [ + "mdi:map-marker-distance", + None, + LENGTH_MILES, + False, + ], + "total_saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], } ATTR_TO_HA_GENERIC = { @@ -104,6 +386,11 @@ ATTR_TO_HA_GENERIC = { "date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, True], "duration": ["mdi:timer-outline", None, TIME_MINUTES, True], "electric_distance_ratio": ["mdi:percent-outline", None, PERCENTAGE, False], + # AllTrips attributes + "battery_size_max": ["mdi:battery-charging-high", None, ENERGY_WATT_HOUR, False], + "reset_date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, False], + "saved_co2": ["mdi:tree-outline", None, MASS_KILOGRAMS, False], + "saved_co2_green_energy": ["mdi:tree-outline", None, MASS_KILOGRAMS, False], } ATTR_TO_HA_METRIC.update(ATTR_TO_HA_GENERIC) @@ -112,6 +399,7 @@ ATTR_TO_HA_IMPERIAL.update(ATTR_TO_HA_GENERIC) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the BMW ConnectedDrive sensors from config entry.""" + # pylint: disable=too-many-nested-blocks if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: attribute_info = ATTR_TO_HA_IMPERIAL else: @@ -145,6 +433,63 @@ async def async_setup_entry(hass, config_entry, async_add_entities): account, vehicle, attribute_name, attribute_info, service ) entities.append(device) + if service == SERVICE_ALL_TRIPS: + for attribute_name in vehicle.state.all_trips.available_attributes: + if attribute_name == "reset_date": + device = BMWConnectedDriveSensor( + account, + vehicle, + "reset_date_utc", + attribute_info, + service, + ) + entities.append(device) + elif attribute_name in ( + "average_combined_consumption", + "average_electric_consumption", + "average_recuperation", + "chargecycle_range", + "total_electric_distance", + ): + for attr in [ + "community_average", + "community_high", + "community_low", + "user_average", + ]: + device = BMWConnectedDriveSensor( + account, + vehicle, + f"{attribute_name}_{attr}", + attribute_info, + service, + ) + entities.append(device) + if attribute_name == "chargecycle_range": + for attr in ["user_current_charge_cycle", "user_high"]: + device = BMWConnectedDriveSensor( + account, + vehicle, + f"{attribute_name}_{attr}", + attribute_info, + service, + ) + entities.append(device) + if attribute_name == "total_electric_distance": + for attr in ["user_total"]: + device = BMWConnectedDriveSensor( + account, + vehicle, + f"{attribute_name}_{attr}", + attribute_info, + service, + ) + entities.append(device) + else: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info, service + ) + entities.append(device) async_add_entities(entities, True) @@ -227,7 +572,6 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state - vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "charging_status": self._state = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: @@ -241,8 +585,28 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): elif self._service is None: self._state = getattr(vehicle_state, self._attribute) elif self._service == SERVICE_LAST_TRIP: + vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "date_utc": date_str = getattr(vehicle_last_trip, "date") self._state = dt_util.parse_datetime(date_str).isoformat() else: self._state = getattr(vehicle_last_trip, self._attribute) + elif self._service == SERVICE_ALL_TRIPS: + vehicle_all_trips = self._vehicle.state.all_trips + for attribute in [ + "average_combined_consumption", + "average_electric_consumption", + "average_recuperation", + "chargecycle_range", + "total_electric_distance", + ]: + if self._attribute.startswith(f"{attribute}_"): + attr = getattr(vehicle_all_trips, attribute) + sub_attr = self._attribute.replace(f"{attribute}_", "") + self._state = getattr(attr, sub_attr) + return + if self._attribute == "reset_date_utc": + date_str = getattr(vehicle_all_trips, "reset_date") + self._state = dt_util.parse_datetime(date_str).isoformat() + else: + self._state = getattr(vehicle_all_trips, self._attribute) From 0f076610fd2d751749986bb98b11718111e01ae6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Jul 2021 16:51:11 -0400 Subject: [PATCH 196/818] Add siren platform (#48309) * Add siren platform * add more supported flags and an ability to set siren duration * tone can be int or string * fix typing * fix typehinting * fix typehints * implement a proposed approach based on discussion * Address comments * fix tests * Small fix * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare * typing * use class attributes * fix naming * remove device from service description * Filter out params from turn on service * fix tests * fix bugs and tests * add test * Combine is_on test with turn on/off/toggle service tests * Update homeassistant/components/siren/__init__.py Co-authored-by: Martin Hjelmare * fix filtering of turn_on attributes * none check * remove services and attributes for volume level, default duration, and default tone * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof * import final * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof * Fix typing and used TypedDict for service parameters * remove is_on function * remove class name redundancy * remove extra service descriptions * switch to positive_int * fix schema for tone Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/siren.py | 83 ++++++++++++ homeassistant/components/siren/__init__.py | 135 +++++++++++++++++++ homeassistant/components/siren/const.py | 17 +++ homeassistant/components/siren/manifest.json | 7 + homeassistant/components/siren/services.yaml | 41 ++++++ script/hassfest/manifest.py | 1 + tests/components/demo/test_siren.py | 108 +++++++++++++++ tests/components/siren/__init__.py | 1 + tests/components/siren/test_init.py | 37 +++++ 11 files changed, 432 insertions(+) create mode 100644 homeassistant/components/demo/siren.py create mode 100644 homeassistant/components/siren/__init__.py create mode 100644 homeassistant/components/siren/const.py create mode 100644 homeassistant/components/siren/manifest.json create mode 100644 homeassistant/components/siren/services.yaml create mode 100644 tests/components/demo/test_siren.py create mode 100644 tests/components/siren/__init__.py create mode 100644 tests/components/siren/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index c420d297afa..ff7dcab2cb3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -442,6 +442,7 @@ homeassistant/components/sighthound/* @robmarkcole homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb +homeassistant/components/siren/* @home-assistant/core @raman325 homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/slack/* @bachya diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index acd98465207..eef341a9c8b 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -22,6 +22,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "number", "select", "sensor", + "siren", "switch", "vacuum", "water_heater", diff --git a/homeassistant/components/demo/siren.py b/homeassistant/components/demo/siren.py new file mode 100644 index 00000000000..b810e48f954 --- /dev/null +++ b/homeassistant/components/demo/siren.py @@ -0,0 +1,83 @@ +"""Demo platform that offers a fake siren device.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.siren import SirenEntity +from homeassistant.components.siren.const import ( + SUPPORT_DURATION, + SUPPORT_TONES, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType + +SUPPORT_FLAGS = SUPPORT_TURN_OFF | SUPPORT_TURN_ON + + +async def async_setup_platform( + hass: HomeAssistant, + config: Config, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: + """Set up the Demo siren devices.""" + async_add_entities( + [ + DemoSiren(name="Siren"), + DemoSiren( + name="Siren with all features", + available_tones=["fire", "alarm"], + support_volume_set=True, + support_duration=True, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo siren devices config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoSiren(SirenEntity): + """Representation of a demo siren device.""" + + def __init__( + self, + name: str, + available_tones: str | None = None, + support_volume_set: bool = False, + support_duration: bool = False, + is_on: bool = True, + ) -> None: + """Initialize the siren device.""" + self._attr_name = name + self._attr_should_poll = False + self._attr_supported_features = SUPPORT_FLAGS + self._attr_is_on = is_on + if available_tones is not None: + self._attr_supported_features |= SUPPORT_TONES + if support_volume_set: + self._attr_supported_features |= SUPPORT_VOLUME_SET + if support_duration: + self._attr_supported_features |= SUPPORT_DURATION + self._attr_available_tones = available_tones + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the siren on.""" + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the siren off.""" + self._attr_is_on = False + self.async_write_ha_state() diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py new file mode 100644 index 00000000000..c9550574bb1 --- /dev/null +++ b/homeassistant/components/siren/__init__.py @@ -0,0 +1,135 @@ +"""Component to interface with various sirens/chimes.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any, TypedDict, cast, final + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import ( + ATTR_AVAILABLE_TONES, + ATTR_DURATION, + ATTR_TONE, + ATTR_VOLUME_LEVEL, + DOMAIN, + SUPPORT_DURATION, + SUPPORT_TONES, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + +TURN_ON_SCHEMA = { + vol.Optional(ATTR_TONE): vol.Any(vol.Coerce(int), cv.string), + vol.Optional(ATTR_DURATION): cv.positive_int, + vol.Optional(ATTR_VOLUME_LEVEL): cv.small_float, +} + + +class SirenTurnOnServiceParameters(TypedDict, total=False): + """Represent possible parameters to siren.turn_on service data dict type.""" + + tone: int | str + duration: int + volume_level: float + + +def filter_turn_on_params( + siren: SirenEntity, params: SirenTurnOnServiceParameters +) -> SirenTurnOnServiceParameters: + """Filter out params not supported by the siren.""" + supported_features = siren.supported_features or 0 + + if not supported_features & SUPPORT_TONES: + params.pop(ATTR_TONE, None) + if not supported_features & SUPPORT_DURATION: + params.pop(ATTR_DURATION, None) + if not supported_features & SUPPORT_VOLUME_SET: + params.pop(ATTR_VOLUME_LEVEL, None) + + return params + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up siren devices.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + async def async_handle_turn_on_service( + siren: SirenEntity, call: ServiceCall + ) -> None: + """Handle turning a siren on.""" + await siren.async_turn_on( + **filter_turn_on_params( + siren, cast(SirenTurnOnServiceParameters, dict(call.data)) + ) + ) + + component.async_register_entity_service( + SERVICE_TURN_ON, TURN_ON_SCHEMA, async_handle_turn_on_service, [SUPPORT_TURN_ON] + ) + component.async_register_entity_service( + SERVICE_TURN_OFF, {}, "async_turn_off", [SUPPORT_TURN_OFF] + ) + component.async_register_entity_service( + SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_TURN_ON & SUPPORT_TURN_OFF] + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class SirenEntity(ToggleEntity): + """Representation of a siren device.""" + + _attr_available_tones: list[int | str] | None = None + + @final + @property + def capability_attributes(self) -> dict[str, Any] | None: + """Return capability attributes.""" + supported_features = self.supported_features or 0 + + if supported_features & SUPPORT_TONES and self.available_tones is not None: + return {ATTR_AVAILABLE_TONES: self.available_tones} + + return None + + @property + def available_tones(self) -> list[int | str] | None: + """ + Return a list of available tones. + + Requires SUPPORT_TONES. + """ + return self._attr_available_tones diff --git a/homeassistant/components/siren/const.py b/homeassistant/components/siren/const.py new file mode 100644 index 00000000000..2faab9ed8e8 --- /dev/null +++ b/homeassistant/components/siren/const.py @@ -0,0 +1,17 @@ +"""Constants for the siren component.""" + +from typing import Final + +DOMAIN: Final = "siren" + +ATTR_TONE: Final = "tone" + +ATTR_AVAILABLE_TONES: Final = "available_tones" +ATTR_DURATION: Final = "duration" +ATTR_VOLUME_LEVEL: Final = "volume_level" + +SUPPORT_TURN_ON: Final = 1 +SUPPORT_TURN_OFF: Final = 2 +SUPPORT_TONES: Final = 4 +SUPPORT_VOLUME_SET: Final = 8 +SUPPORT_DURATION: Final = 16 diff --git a/homeassistant/components/siren/manifest.json b/homeassistant/components/siren/manifest.json new file mode 100644 index 00000000000..454835c33b0 --- /dev/null +++ b/homeassistant/components/siren/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "siren", + "name": "Siren", + "documentation": "https://www.home-assistant.io/integrations/siren", + "codeowners": ["@home-assistant/core", "@raman325"], + "quality_scale": "internal" +} \ No newline at end of file diff --git a/homeassistant/components/siren/services.yaml b/homeassistant/components/siren/services.yaml new file mode 100644 index 00000000000..8c5ed3be974 --- /dev/null +++ b/homeassistant/components/siren/services.yaml @@ -0,0 +1,41 @@ +# Describes the format for available siren services + +turn_on: + description: Turn siren on. + target: + entity: + domain: siren + fields: + tone: + description: The tone to emit when turning the siren on. Must be supported by the integration. + example: fire + required: false + selector: + text: + volume_level: + description: The volume level of the noise to emit when turning the siren on. Must be supported by the integration. + example: 0.5 + required: false + selector: + number: + min: 0 + max: 1 + step: 0.05 + duration: + description: The duration in seconds of the noise to emit when turning the siren on. Must be supported by the integration. + example: 15 + required: false + selector: + text: + +turn_off: + description: Turn siren off. + target: + entity: + domain: siren + +toggle: + description: Toggles a siren. + target: + entity: + domain: siren diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 00110e11fbc..797729542f4 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -93,6 +93,7 @@ NO_IOT_CLASS = [ "search", "select", "sensor", + "siren", "stt", "switch", "system_health", diff --git a/tests/components/demo/test_siren.py b/tests/components/demo/test_siren.py new file mode 100644 index 00000000000..74c39c668e9 --- /dev/null +++ b/tests/components/demo/test_siren.py @@ -0,0 +1,108 @@ +"""The tests for the demo siren component.""" +from unittest.mock import call, patch + +import pytest + +from homeassistant.components.siren.const import ( + ATTR_AVAILABLE_TONES, + ATTR_VOLUME_LEVEL, + DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +ENTITY_SIREN = "siren.siren" +ENTITY_SIREN_WITH_ALL_FEATURES = "siren.siren_with_all_features" + + +@pytest.fixture(autouse=True) +async def setup_demo_siren(hass): + """Initialize setup demo siren.""" + assert await async_setup_component(hass, DOMAIN, {"siren": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass): + """Test the initial parameters.""" + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_ON + assert ATTR_AVAILABLE_TONES not in state.attributes + + +def test_all_setup_params(hass): + """Test the setup with all parameters.""" + state = hass.states.get(ENTITY_SIREN_WITH_ALL_FEATURES) + assert state.attributes.get(ATTR_AVAILABLE_TONES) == ["fire", "alarm"] + + +async def test_turn_on(hass): + """Test turn on device.""" + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_OFF + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_ON + + +async def test_turn_off(hass): + """Test turn off device.""" + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_ON + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_OFF + + +async def test_toggle(hass): + """Test toggle device.""" + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_ON + + await hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_OFF + + await hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True + ) + state = hass.states.get(ENTITY_SIREN) + assert state.state == STATE_ON + + +async def test_turn_on_strip_attributes(hass): + """Test attributes are stripped from turn_on service call when not supported.""" + with patch( + "homeassistant.components.demo.siren.DemoSiren.async_turn_on" + ) as svc_call: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_SIREN, ATTR_VOLUME_LEVEL: 1}, + blocking=True, + ) + assert svc_call.called + assert svc_call.call_args_list[0] == call(**{ATTR_ENTITY_ID: [ENTITY_SIREN]}) diff --git a/tests/components/siren/__init__.py b/tests/components/siren/__init__.py new file mode 100644 index 00000000000..a246822bdc5 --- /dev/null +++ b/tests/components/siren/__init__.py @@ -0,0 +1 @@ +"""Tests for the siren component.""" diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py new file mode 100644 index 00000000000..e3b8bded6c8 --- /dev/null +++ b/tests/components/siren/test_init.py @@ -0,0 +1,37 @@ +"""The tests for the siren component.""" +from unittest.mock import MagicMock + +from homeassistant.components.siren import SirenEntity + + +class MockSirenEntity(SirenEntity): + """Mock siren device to use in tests.""" + + _attr_is_on = True + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return 0 + + +async def test_sync_turn_on(hass): + """Test if async turn_on calls sync turn_on.""" + siren = MockSirenEntity() + siren.hass = hass + + siren.turn_on = MagicMock() + await siren.async_turn_on() + + assert siren.turn_on.called + + +async def test_sync_turn_off(hass): + """Test if async turn_off calls sync turn_off.""" + siren = MockSirenEntity() + siren.hass = hass + + siren.turn_off = MagicMock() + await siren.async_turn_off() + + assert siren.turn_off.called From 574cb03acc6e1479923b161eb5356f08aa6cb1f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Jul 2021 11:03:48 -1000 Subject: [PATCH 197/818] Send ssdp requests to ipv4 broadcast as well (#52760) * Send ssdp requests to 255.255.255.255 as well - This matches pysonos behavior and may fix reports of inability to discover some sonos devices https://github.com/amelchio/pysonos/blob/master/pysonos/discovery.py#L120 * Update homeassistant/components/ssdp/__init__.py --- homeassistant/components/ssdp/__init__.py | 17 ++++++++++++- tests/components/ssdp/test_init.py | 31 +++++++++++++---------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d03f8967311..9896ec4177e 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -29,6 +29,8 @@ from .flow import FlowDispatcher, SSDPFlow DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) +IPV4_BROADCAST = IPv4Address("255.255.255.255") + # Attributes for accessing info from SSDP response ATTR_SSDP_LOCATION = "ssdp_location" ATTR_SSDP_ST = "ssdp_st" @@ -236,7 +238,20 @@ class Scanner: async_callback=self._async_process_entry, source_ip=source_ip ) ) - + try: + IPv4Address(source_ip) + except ValueError: + continue + # Some sonos devices only seem to respond if we send to the broadcast + # address. This matches pysonos' behavior + # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 + self._ssdp_listeners.append( + SSDPListener( + async_callback=self._async_process_entry, + source_ip=source_ip, + target_ip=IPV4_BROADCAST, + ) + ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 6c019f1f311..568a2261fee 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -295,15 +295,15 @@ async def test_start_stop_scanner(async_start_mock, async_search_mock, hass): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_start_mock.call_count == 1 - assert async_search_mock.call_count == 1 + assert async_start_mock.call_count == 2 + assert async_search_mock.call_count == 2 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_start_mock.call_count == 1 - assert async_search_mock.call_count == 1 + assert async_start_mock.call_count == 2 + assert async_search_mock.call_count == 2 async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog): @@ -459,11 +459,11 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): await hass.async_block_till_done() assert hass.state == CoreState.running - assert len(integration_callbacks) == 3 - assert len(integration_callbacks_from_cache) == 3 - assert len(integration_match_all_callbacks) == 3 + assert len(integration_callbacks) == 5 + assert len(integration_callbacks_from_cache) == 5 + assert len(integration_match_all_callbacks) == 5 assert len(integration_match_all_not_present_callbacks) == 0 - assert len(match_any_callbacks) == 3 + assert len(match_any_callbacks) == 5 assert len(not_matching_integration_callbacks) == 0 assert integration_callbacks[0] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", @@ -546,7 +546,7 @@ async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog assert hass.state == CoreState.running assert ( - len(integration_callbacks) == 2 + len(integration_callbacks) == 4 ) # unsolicited callbacks without st are not cached assert integration_callbacks[0] == { "UDN": "uuid:RINCON_1111BB963FD801400", @@ -635,7 +635,7 @@ async def test_scan_second_hit(hass, aioclient_mock, caplog): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert len(integration_callbacks) == 2 + assert len(integration_callbacks) == 4 assert integration_callbacks[0] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_SSDP_EXT: "", @@ -781,7 +781,12 @@ async def test_async_detect_interfaces_setting_empty_route(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert {create_args[0][1]["source_ip"], create_args[1][1]["source_ip"]} == { - IPv4Address("192.168.1.5"), - IPv6Address("2001:db8::"), + argset = set() + for argmap in create_args: + argset.add((argmap[1].get("source_ip"), argmap[1].get("target_ip"))) + + assert argset == { + (IPv6Address("2001:db8::"), None), + (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), + (IPv4Address("192.168.1.5"), None), } From e5567222819a9ed4589258af65a06cac9b1aad63 Mon Sep 17 00:00:00 2001 From: Leszek Swirski Date: Mon, 12 Jul 2021 00:51:28 +0200 Subject: [PATCH 198/818] Add device classes to homematicip_cloud cover (#52793) Make HMIP covers report a SHUTTER/BLIND/GARAGE device_class (as appropriate). --- .../components/homematicip_cloud/cover.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 2d3e1ea518c..843f44510c1 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -15,6 +15,9 @@ from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHUTTER, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -63,6 +66,11 @@ async def async_setup_entry( class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): """Representation of the HomematicIP blind module.""" + @property + def device_class(self) -> str: + """Return the class of the cover.""" + return DEVICE_CLASS_BLIND + @property def current_cover_position(self) -> int: """Return current position of cover.""" @@ -151,6 +159,11 @@ class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): hap, device, channel=channel, is_multi_channel=is_multi_channel ) + @property + def device_class(self) -> str: + """Return the class of the cover.""" + return DEVICE_CLASS_SHUTTER + @property def current_cover_position(self) -> int: """Return current position of cover.""" @@ -264,6 +277,11 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): } return door_state_to_position.get(self._device.doorState) + @property + def device_class(self) -> str: + """Return the class of the cover.""" + return DEVICE_CLASS_GARAGE + @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" @@ -290,6 +308,11 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): device.modelType = f"HmIP-{post}" super().__init__(hap, device, post, is_multi_channel=False) + @property + def device_class(self) -> str: + """Return the class of the cover.""" + return DEVICE_CLASS_SHUTTER + @property def current_cover_position(self) -> int: """Return current position of cover.""" From 2ddaf746e6ab5ce9a616c1a671f59513f7875084 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jul 2021 00:09:46 +0000 Subject: [PATCH 199/818] [ci skip] Translation update --- .../components/dsmr/translations/fr.json | 11 ++++++- .../forecast_solar/translations/fr.json | 11 +++++++ .../components/select/translations/fr.json | 3 ++ .../xiaomi_miio/translations/fr.json | 30 +++++++++++++++++++ .../components/zwave_js/translations/fr.json | 14 +++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/forecast_solar/translations/fr.json create mode 100644 homeassistant/components/select/translations/fr.json diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json index d156aee8ca0..3f462c74c05 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -9,7 +9,16 @@ }, "step": { "one": "", - "other": "Autre" + "other": "Autre", + "setup_serial": { + "data": { + "port": "S\u00e9lectionner un appareil" + }, + "title": "Appareil" + }, + "setup_serial_manual_path": { + "title": "Chemin" + } } }, "options": { diff --git a/homeassistant/components/forecast_solar/translations/fr.json b/homeassistant/components/forecast_solar/translations/fr.json new file mode 100644 index 00000000000..a6091b9c21b --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/fr.json b/homeassistant/components/select/translations/fr.json new file mode 100644 index 00000000000..7a3633cb309 --- /dev/null +++ b/homeassistant/components/select/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "S\u00e9lectionner" +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 30def127e7a..82b2d4991cb 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -11,6 +11,17 @@ }, "flow_title": "Xiaomi Miio: {name}", "step": { + "cloud": { + "data": { + "cloud_password": "Mot de passe cloud", + "cloud_username": "Nom d'utilisateur cloud" + } + }, + "connect": { + "data": { + "model": "Mod\u00e8le d'appareil" + } + }, "device": { "data": { "host": "Adresse IP", @@ -30,6 +41,11 @@ "description": "Vous aurez besoin du jeton API, voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions.", "title": "Se connecter \u00e0 la passerelle Xiaomi" }, + "select": { + "data": { + "select_device": "Appareil Miio" + } + }, "user": { "data": { "gateway": "Se connecter \u00e0 la passerelle Xiaomi" @@ -38,5 +54,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Identifiants cloud incomplets, veuillez renseigner le nom d'utilisateur, le mot de passe et le pays" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Utiliser le cloud pour connecter des sous-appareils" + }, + "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 33571f12d60..49bb760ac2a 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -51,5 +51,19 @@ } } }, + "options": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_ws_url": "URL websocket invalide" + }, + "step": { + "configure_addon": { + "data": { + "log_level": "Niveau du journal", + "network_key": "Cl\u00e9 r\u00e9seau" + } + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file From e652ef51a16350a9f77c7c518f3a9415a2d62e0f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Jul 2021 00:22:33 -0400 Subject: [PATCH 200/818] Initial support for zwave_js device conditions (#52003) --- homeassistant/components/zwave_js/const.py | 3 + .../components/zwave_js/device_condition.py | 217 +++++++ homeassistant/components/zwave_js/helpers.py | 34 +- .../components/zwave_js/strings.json | 17 +- .../components/zwave_js/translations/en.json | 10 + .../zwave_js/test_device_condition.py | 572 ++++++++++++++++++ 6 files changed, 848 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/zwave_js/device_condition.py create mode 100644 tests/components/zwave_js/test_device_condition.py diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 9e6e37b4ee7..ae390ce4581 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -45,6 +45,9 @@ ATTR_EVENT_DATA = "event_data" ATTR_DATA_TYPE = "data_type" ATTR_WAIT_FOR_RESULT = "wait_for_result" +ATTR_NODE = "node" +ATTR_ZWAVE_VALUE = "zwave_value" + # service constants ATTR_NODES = "nodes" diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py new file mode 100644 index 00000000000..b419230a0bd --- /dev/null +++ b/homeassistant/components/zwave_js/device_condition.py @@ -0,0 +1,217 @@ +"""Provide the device conditions for Z-Wave JS.""" +from __future__ import annotations + +from typing import cast + +import voluptuous as vol +from zwave_js_server.const import CommandClass, ConfigurationValueType +from zwave_js_server.model.value import ConfigurationValue + +from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_ENDPOINT, + ATTR_PROPERTY, + ATTR_PROPERTY_KEY, + ATTR_VALUE, +) +from .helpers import async_get_node_from_device_id, get_zwave_value_from_config + +CONF_SUBTYPE = "subtype" +CONF_VALUE_ID = "value_id" +CONF_STATUS = "status" + +NODE_STATUS_TYPE = "node_status" +NODE_STATUS_TYPES = ["asleep", "awake", "dead", "alive"] +CONFIG_PARAMETER_TYPE = "config_parameter" +VALUE_TYPE = "value" +CONDITION_TYPES = {NODE_STATUS_TYPE, CONFIG_PARAMETER_TYPE, VALUE_TYPE} + +NODE_STATUS_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): NODE_STATUS_TYPE, + vol.Required(CONF_STATUS): vol.In(NODE_STATUS_TYPES), + } +) + +CONFIG_PARAMETER_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): CONFIG_PARAMETER_TYPE, + vol.Required(CONF_VALUE_ID): cv.string, + vol.Required(CONF_SUBTYPE): cv.string, + vol.Optional(ATTR_VALUE): vol.Coerce(int), + } +) + +VALUE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): VALUE_TYPE, + vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]), + vol.Required(ATTR_PROPERTY): vol.Any(vol.Coerce(int), cv.string), + vol.Optional(ATTR_PROPERTY_KEY): vol.Any(vol.Coerce(int), cv.string), + vol.Optional(ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(ATTR_VALUE): vol.Any( + bool, + vol.Coerce(int), + vol.Coerce(float), + cv.boolean, + cv.string, + ), + } +) + +CONDITION_SCHEMA = vol.Any( + NODE_STATUS_CONDITION_SCHEMA, + CONFIG_PARAMETER_CONDITION_SCHEMA, + VALUE_CONDITION_SCHEMA, +) + + +async def async_validate_condition_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == VALUE_TYPE: + node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID]) + get_zwave_value_from_config(node, config) + + return config + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device conditions for Z-Wave JS devices.""" + conditions = [] + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + } + node = async_get_node_from_device_id(hass, device_id) + + # Any value's value condition + conditions.append({**base_condition, CONF_TYPE: VALUE_TYPE}) + + # Node status conditions + conditions.append({**base_condition, CONF_TYPE: NODE_STATUS_TYPE}) + + # Config parameter conditions + conditions.extend( + [ + { + **base_condition, + CONF_VALUE_ID: config_value.value_id, + CONF_TYPE: CONFIG_PARAMETER_TYPE, + CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})", + } + for config_value in node.get_configuration_values().values() + ] + ) + + return conditions + + +@callback +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + condition_type = config[CONF_TYPE] + device_id = config[CONF_DEVICE_ID] + + @callback + def test_node_status(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if node status is a certain state.""" + node = async_get_node_from_device_id(hass, device_id) + return bool(node.status.name.lower() == config[CONF_STATUS]) + + if condition_type == NODE_STATUS_TYPE: + return test_node_status + + @callback + def test_config_parameter(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if config parameter is a certain state.""" + node = async_get_node_from_device_id(hass, device_id) + config_value = cast(ConfigurationValue, node.values[config[CONF_VALUE_ID]]) + return bool(config_value.value == config[ATTR_VALUE]) + + if condition_type == CONFIG_PARAMETER_TYPE: + return test_config_parameter + + @callback + def test_value(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if value is a certain state.""" + node = async_get_node_from_device_id(hass, device_id) + value = get_zwave_value_from_config(node, config) + return bool(value.value == config[ATTR_VALUE]) + + if condition_type == VALUE_TYPE: + return test_value + + raise HomeAssistantError(f"Unhandled condition type {condition_type}") + + +@callback +async def async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List condition capabilities.""" + device_id = config[CONF_DEVICE_ID] + node = async_get_node_from_device_id(hass, device_id) + + # Add additional fields to the automation trigger UI + if config[CONF_TYPE] == CONFIG_PARAMETER_TYPE: + value_id = config[CONF_VALUE_ID] + config_value = cast(ConfigurationValue, node.values[value_id]) + min_ = config_value.metadata.min + max_ = config_value.metadata.max + + if config_value.configuration_value_type in ( + ConfigurationValueType.RANGE, + ConfigurationValueType.MANUAL_ENTRY, + ): + value_schema = vol.Range(min=min_, max=max_) + elif config_value.configuration_value_type == ConfigurationValueType.ENUMERATED: + value_schema = vol.In( + {int(k): v for k, v in config_value.metadata.states.items()} + ) + else: + return {} + + return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})} + + if config[CONF_TYPE] == VALUE_TYPE: + return { + "extra_fields": vol.Schema( + { + vol.Required(ATTR_COMMAND_CLASS): vol.In( + {cc.value: cc.name for cc in CommandClass} + ), + vol.Required(ATTR_PROPERTY): cv.string, + vol.Optional(ATTR_PROPERTY_KEY): cv.string, + vol.Optional(ATTR_ENDPOINT): cv.string, + vol.Required(ATTR_VALUE): cv.string, + } + ) + } + + if config[CONF_TYPE] == NODE_STATUS_TYPE: + return { + "extra_fields": vol.Schema( + {vol.Required(CONF_STATUS): vol.In(NODE_STATUS_TYPES)} + ) + } + + return {} diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 81eae0fdc15..f8e8dab2b46 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -3,9 +3,10 @@ from __future__ import annotations from typing import Any, cast +import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import Value as ZwaveValue +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.const import __version__ as HA_VERSION @@ -18,8 +19,17 @@ from homeassistant.helpers.entity_registry import ( EntityRegistry, async_get as async_get_ent_reg, ) +from homeassistant.helpers.typing import ConfigType -from .const import CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_ENDPOINT, + ATTR_PROPERTY, + ATTR_PROPERTY_KEY, + CONF_DATA_COLLECTION_OPTED_IN, + DATA_CLIENT, + DOMAIN, +) @callback @@ -143,3 +153,23 @@ def async_get_node_from_entity_id( # tied to a device assert entity_entry.device_id return async_get_node_from_device_id(hass, entity_entry.device_id, dev_reg) + + +def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveValue: + """Get a Z-Wave JS Value from a config.""" + endpoint = None + if config.get(ATTR_ENDPOINT): + endpoint = config[ATTR_ENDPOINT] + property_key = None + if config.get(ATTR_PROPERTY_KEY): + property_key = config[ATTR_PROPERTY_KEY] + value_id = get_value_id( + node, + config[ATTR_COMMAND_CLASS], + config[ATTR_PROPERTY], + endpoint, + property_key, + ) + if value_id not in node.values: + raise vol.Invalid(f"Value {value_id} can't be found on node {node}") + return node.values[value_id] diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index b942a75b27a..1595ee58889 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -10,7 +10,9 @@ "on_supervisor": { "title": "Select connection method", "description": "Do you want to use the Z-Wave JS Supervisor add-on?", - "data": { "use_addon": "Use the Z-Wave JS Supervisor add-on" } + "data": { + "use_addon": "Use the Z-Wave JS Supervisor add-on" + } }, "install_addon": { "title": "The Z-Wave JS add-on installation has started" @@ -22,7 +24,9 @@ "network_key": "Network Key" } }, - "start_addon": { "title": "The Z-Wave JS add-on is starting." }, + "start_addon": { + "title": "The Z-Wave JS add-on is starting." + }, "hassio_confirm": { "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" } @@ -93,5 +97,12 @@ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." } + }, + "device_automation": { + "condition_type": { + "node_status": "Node status", + "config_parameter": "Config parameter {subtype} value", + "value": "Current value of a Z-Wave Value" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 27cafb6af6e..af63014f588 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -101,5 +101,15 @@ } } }, + "device_automation": { + "condition_type": { + "alive": "Node is alive", + "asleep": "Node is asleep", + "awake": "Node is awake", + "config_parameter": "Config parameter {subtype} value", + "dead": "Node is dead", + "value": "Current value of a Z-Wave Value" + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py new file mode 100644 index 00000000000..eef672c4c5b --- /dev/null +++ b/tests/components/zwave_js/test_device_condition.py @@ -0,0 +1,572 @@ +"""The tests for Z-Wave JS device conditions.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest +import voluptuous as vol +import voluptuous_serialize +from zwave_js_server.const import CommandClass +from zwave_js_server.event import Event + +from homeassistant.components import automation +from homeassistant.components.zwave_js import DOMAIN, device_condition +from homeassistant.components.zwave_js.helpers import get_zwave_value_from_config +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.setup import async_setup_component + +from tests.common import async_get_device_automations, async_mock_service + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, client, lock_schlage_be469, integration) -> None: + """Test we get the expected onditions from a zwave_js.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + config_value = list(lock_schlage_be469.get_configuration_values().values())[0] + value_id = config_value.value_id + name = config_value.property_name + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "node_status", + "device_id": device.id, + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "config_parameter", + "device_id": device.id, + "value_id": value_id, + "subtype": f"{value_id} ({name})", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "value", + "device_id": device.id, + }, + ] + conditions = await async_get_device_automations(hass, "condition", device.id) + for condition in expected_conditions: + assert condition in conditions + + +async def test_node_status_state( + hass, client, lock_schlage_be469, integration, calls +) -> None: + """Test for node_status conditions.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "node_status", + "status": "alive", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "alive - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "node_status", + "status": "awake", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "awake - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "node_status", + "status": "asleep", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "asleep - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "node_status", + "status": "dead", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "dead - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "alive - event - test_event1" + + event = Event( + "wake up", + data={ + "source": "node", + "event": "wake up", + "nodeId": lock_schlage_be469.node_id, + }, + ) + lock_schlage_be469.receive_event(event) + await hass.async_block_till_done() + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "awake - event - test_event2" + + event = Event( + "sleep", + data={"source": "node", "event": "sleep", "nodeId": lock_schlage_be469.node_id}, + ) + lock_schlage_be469.receive_event(event) + await hass.async_block_till_done() + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "asleep - event - test_event3" + + event = Event( + "dead", + data={"source": "node", "event": "dead", "nodeId": lock_schlage_be469.node_id}, + ) + lock_schlage_be469.receive_event(event) + await hass.async_block_till_done() + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "dead - event - test_event4" + + event = Event( + "unknown", + data={ + "source": "node", + "event": "unknown", + "nodeId": lock_schlage_be469.node_id, + }, + ) + lock_schlage_be469.receive_event(event) + await hass.async_block_till_done() + + +async def test_config_parameter_state( + hass, client, lock_schlage_be469, integration, calls +) -> None: + """Test for config_parameter conditions.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "config_parameter", + "value_id": f"{lock_schlage_be469.node_id}-112-0-3", + "subtype": f"{lock_schlage_be469.node_id}-112-0-3 (Beeper)", + "value": 255, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "Beeper - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "config_parameter", + "value_id": f"{lock_schlage_be469.node_id}-112-0-6", + "subtype": f"{lock_schlage_be469.node_id}-112-0-6 (User Slot Status)", + "value": 1, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "User Slot Status - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "Beeper - event - test_event1" + + # Flip Beeper state to not match condition + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": lock_schlage_be469.node_id, + "args": { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "newValue": 0, + "prevValue": 255, + }, + }, + ) + lock_schlage_be469.receive_event(event) + + # Flip User Slot Status to match condition + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": lock_schlage_be469.node_id, + "args": { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "newValue": 1, + "prevValue": 117440512, + }, + }, + ) + lock_schlage_be469.receive_event(event) + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "User Slot Status - event - test_event2" + + +async def test_value_state( + hass, client, lock_schlage_be469, integration, calls +) -> None: + """Test for value conditions.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "value", + "command_class": 112, + "property": 3, + "value": 255, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "value - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "value - event - test_event1" + + +async def test_get_condition_capabilities_node_status( + hass, client, lock_schlage_be469, integration +): + """Test we don't get capabilities from a node_status condition.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "node_status", + }, + ) + assert capabilities and "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "status", + "required": True, + "type": "select", + "options": [ + ("asleep", "asleep"), + ("awake", "awake"), + ("dead", "dead"), + ("alive", "alive"), + ], + } + ] + + +async def test_get_condition_capabilities_value( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a value condition.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "value", + }, + ) + assert capabilities and "extra_fields" in capabilities + + cc_options = [(cc.value, cc.name) for cc in CommandClass] + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "command_class", + "required": True, + "options": cc_options, + "type": "select", + }, + {"name": "property", "required": True, "type": "string"}, + {"name": "property_key", "optional": True, "type": "string"}, + {"name": "endpoint", "optional": True, "type": "string"}, + {"name": "value", "required": True, "type": "string"}, + ] + + +async def test_get_condition_capabilities_config_parameter( + hass, client, climate_radio_thermostat_ct100_plus, integration +): + """Test we get the expected capabilities from a config_parameter condition.""" + node = climate_radio_thermostat_ct100_plus + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + # Test enumerated type param + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "config_parameter", + "value_id": f"{node.node_id}-112-0-1", + "subtype": f"{node.node_id}-112-0-1 (Temperature Reporting Threshold)", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "value", + "required": True, + "options": [ + (0, "Disabled"), + (1, "0.5° F"), + (2, "1.0° F"), + (3, "1.5° F"), + (4, "2.0° F"), + ], + "type": "select", + } + ] + + # Test range type param + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "config_parameter", + "value_id": f"{node.node_id}-112-0-10", + "subtype": f"{node.node_id}-112-0-10 (Temperature Reporting Filter)", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "value", + "required": True, + "valueMin": 0, + "valueMax": 124, + } + ] + + # Test undefined type param + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "config_parameter", + "value_id": f"{node.node_id}-112-0-2", + "subtype": f"{node.node_id}-112-0-2 (HVAC Settings)", + }, + ) + assert not capabilities + + +async def test_failure_scenarios(hass, client, hank_binary_switch, integration): + """Test failure scenarios.""" + dev_reg = device_registry.async_get(hass) + device = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + )[0] + + with pytest.raises(HomeAssistantError): + await device_condition.async_condition_from_config( + {"type": "failed.test", "device_id": device.id}, False + ) + + with patch( + "homeassistant.components.zwave_js.device_condition.async_get_node_from_device_id", + return_value=None, + ), patch( + "homeassistant.components.zwave_js.device_condition.get_zwave_value_from_config", + return_value=None, + ): + assert ( + await device_condition.async_get_condition_capabilities( + hass, {"type": "failed.test", "device_id": device.id} + ) + == {} + ) + + +async def test_get_value_from_config_failure( + hass, client, hank_binary_switch, integration +): + """Test get_value_from_config invalid value ID.""" + with pytest.raises(vol.Invalid): + get_zwave_value_from_config( + hank_binary_switch, + { + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": 15, + "endpoint": 10, + }, + ) From 91a2b96da0bbd3bb611a67ee44ed71cdb0748804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 12 Jul 2021 07:25:00 +0300 Subject: [PATCH 201/818] Implement stable unique id for Huawei LTE, requires credentials on setup (#49878) --- .../components/huawei_lte/__init__.py | 154 ++++++++++++------ .../components/huawei_lte/binary_sensor.py | 3 +- .../components/huawei_lte/config_flow.py | 141 ++++++++-------- homeassistant/components/huawei_lte/const.py | 4 + .../components/huawei_lte/device_tracker.py | 15 +- .../components/huawei_lte/manifest.json | 1 - homeassistant/components/huawei_lte/notify.py | 6 +- homeassistant/components/huawei_lte/sensor.py | 3 +- .../components/huawei_lte/strings.json | 7 +- homeassistant/components/huawei_lte/switch.py | 3 +- homeassistant/components/huawei_lte/utils.py | 23 +++ requirements_all.txt | 1 - requirements_test_all.txt | 1 - .../components/huawei_lte/test_config_flow.py | 31 +++- 14 files changed, 231 insertions(+), 162 deletions(-) create mode 100644 homeassistant/components/huawei_lte/utils.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 7503c1d5e71..81b715b71fe 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -4,15 +4,11 @@ from __future__ import annotations from collections import defaultdict from contextlib import suppress from datetime import timedelta -from functools import partial -import ipaddress import logging import time from typing import Any, Callable, cast -from urllib.parse import urlparse import attr -from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection @@ -34,6 +30,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_RECIPIENT, @@ -41,12 +38,13 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, discovery, + entity_registry, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity @@ -56,6 +54,8 @@ from homeassistant.helpers.typing import ConfigType from .const import ( ADMIN_SERVICES, ALL_KEYS, + ATTR_UNIQUE_ID, + CONF_UNAUTHENTICATED_MODE, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME, @@ -81,6 +81,7 @@ from .const import ( SERVICE_SUSPEND_INTEGRATION, UPDATE_SIGNAL, ) +from .utils import get_device_macs _LOGGER = logging.getLogger(__name__) @@ -131,11 +132,10 @@ CONFIG_ENTRY_PLATFORMS = ( class Router: """Class for router state.""" + hass: HomeAssistant = attr.ib() config_entry: ConfigEntry = attr.ib() connection: Connection = attr.ib() url: str = attr.ib() - mac: str = attr.ib() - signal_update: CALLBACK_TYPE = attr.ib() data: dict[str, Any] = attr.ib(init=False, factory=dict) subscriptions: dict[str, set[str]] = attr.ib( @@ -165,15 +165,15 @@ class Router: @property def device_identifiers(self) -> set[tuple[str, str]]: """Get router identifiers for device registry.""" - try: - return {(DOMAIN, self.data[KEY_DEVICE_INFORMATION]["SerialNumber"])} - except (KeyError, TypeError): - return set() + assert self.config_entry.unique_id is not None + return {(DOMAIN, self.config_entry.unique_id)} @property def device_connections(self) -> set[tuple[str, str]]: """Get router connections for device registry.""" - return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() + return { + (dr.CONNECTION_NETWORK_MAC, x) for x in self.config_entry.data[CONF_MAC] + } def _get_data(self, key: str, func: Callable[[], Any]) -> None: if not self.subscriptions.get(key): @@ -271,7 +271,7 @@ class Router: KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch ) - self.signal_update() + dispatcher_send(self.hass, UPDATE_SIGNAL, self.config_entry.unique_id) def logout(self) -> None: """Log out router session.""" @@ -304,7 +304,9 @@ class HuaweiLteData: routers: dict[str, Router] = attr.ib(init=False, factory=dict) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up Huawei LTE component from config entry.""" url = entry.data[CONF_URL] @@ -342,61 +344,92 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options={**entry.options, **new_options}, ) - # Get MAC address for use in unique ids. Being able to use something - # from the API would be nice, but all of that seems to be available only - # through authenticated calls (e.g. device_information.SerialNumber), and - # we want this available and the same when unauthenticated too. - host = urlparse(url).hostname - try: - if ipaddress.ip_address(host).version == 6: - mode = "ip6" - else: - mode = "ip" - except ValueError: - mode = "hostname" - mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) - def get_connection() -> Connection: - """ - Set up a connection. - - Authorized one if username/pass specified (even if empty), unauthorized one otherwise. - """ - username = entry.data.get(CONF_USERNAME) - password = entry.data.get(CONF_PASSWORD) - if username or password: - connection: Connection = AuthorizedConnection( + """Set up a connection.""" + if entry.options.get(CONF_UNAUTHENTICATED_MODE): + _LOGGER.debug("Connecting in unauthenticated mode, reduced feature set") + connection = Connection(url, timeout=CONNECTION_TIMEOUT) + else: + _LOGGER.debug("Connecting in authenticated mode, full feature set") + username = entry.data.get(CONF_USERNAME) or "" + password = entry.data.get(CONF_PASSWORD) or "" + connection = AuthorizedConnection( url, username=username, password=password, timeout=CONNECTION_TIMEOUT ) - else: - connection = Connection(url, timeout=CONNECTION_TIMEOUT) return connection - def signal_update() -> None: - """Signal updates to data.""" - dispatcher_send(hass, UPDATE_SIGNAL, url) - try: connection = await hass.async_add_executor_job(get_connection) except Timeout as ex: raise ConfigEntryNotReady from ex - # Set up router and store reference to it - router = Router(entry, connection, url, mac, signal_update) - hass.data[DOMAIN].routers[url] = router + # Set up router + router = Router(hass, entry, connection, url) # Do initial data update await hass.async_add_executor_job(router.update) + # Check that we found required information + device_info = router.data.get(KEY_DEVICE_INFORMATION) + if not entry.unique_id: + # Transitional from < 2021.8: update None config entry and entity unique ids + if device_info and (serial_number := device_info.get("SerialNumber")): + hass.config_entries.async_update_entry(entry, unique_id=serial_number) + ent_reg = entity_registry.async_get(hass) + for entity_entry in entity_registry.async_entries_for_config_entry( + ent_reg, entry.entry_id + ): + if not entity_entry.unique_id.startswith("None-"): + continue + new_unique_id = ( + f"{serial_number}-{entity_entry.unique_id.split('-', 1)[1]}" + ) + ent_reg.async_update_entity( + entity_entry.entity_id, new_unique_id=new_unique_id + ) + else: + await hass.async_add_executor_job(router.cleanup) + msg = ( + "Could not resolve serial number to use as unique id for router at %s" + ", setup failed" + ) + if not entry.data.get(CONF_PASSWORD): + msg += ( + ". Try setting up credentials for the router for one startup, " + "unauthenticated mode can be enabled after that in integration " + "settings" + ) + _LOGGER.error(msg, url) + return False + + # Store reference to router + hass.data[DOMAIN].routers[entry.unique_id] = router + # Clear all subscriptions, enabled entities will push back theirs router.subscriptions.clear() + # Update device MAC addresses on record. These can change due to toggling between + # authenticated and unauthenticated modes, or likely also when enabling/disabling + # SSIDs in the router config. + try: + wlan_settings = await hass.async_add_executor_job( + router.client.wlan.multi_basic_settings + ) + except Exception: # pylint: disable=broad-except + # Assume not supported, or authentication required but in unauthenticated mode + wlan_settings = {} + macs = get_device_macs(device_info or {}, wlan_settings) + # Be careful not to overwrite a previous, more complete set with a partial one + if macs and (not entry.data[CONF_MAC] or (device_info and wlan_settings)): + new_data = dict(entry.data) + new_data[CONF_MAC] = macs + hass.config_entries.async_update_entry(entry, data=new_data) + # Set up device registry if router.device_identifiers or router.device_connections: device_data = {} sw_version = None - if router.data.get(KEY_DEVICE_INFORMATION): - device_info = router.data[KEY_DEVICE_INFORMATION] + if device_info: sw_version = device_info.get("SoftwareVersion") if device_info.get("DeviceName"): device_data["model"] = device_info["DeviceName"] @@ -425,7 +458,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: NOTIFY_DOMAIN, DOMAIN, { - CONF_URL: url, + ATTR_UNIQUE_ID: entry.unique_id, CONF_NAME: entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), CONF_RECIPIENT: entry.options.get(CONF_RECIPIENT), }, @@ -462,7 +495,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) # Forget about the router and invoke its cleanup - router = hass.data[DOMAIN].routers.pop(config_entry.data[CONF_URL]) + router = hass.data[DOMAIN].routers.pop(config_entry.unique_id) await hass.async_add_executor_job(router.cleanup) return True @@ -483,10 +516,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config def service_handler(service: ServiceCall) -> None: - """Apply a service.""" + """ + Apply a service. + + We key this using the router URL instead of its unique id / serial number, + because the latter is not available anywhere in the UI. + """ routers = hass.data[DOMAIN].routers if url := service.data.get(CONF_URL): - router = routers.get(url) + router = next( + (router for router in routers.values() if router.url == url), None + ) elif not routers: _LOGGER.error("%s: no routers configured", service.service) return @@ -496,7 +536,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error( "%s: more than one router configured, must specify one of URLs %s", service.service, - sorted(routers), + sorted(router.url for router in routers.values()), ) return if not router: @@ -560,6 +600,12 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry.version = 2 hass.config_entries.async_update_entry(config_entry, options=options) _LOGGER.info("Migrated config entry to version %d", config_entry.version) + if config_entry.version == 2: + config_entry.version = 3 + data = dict(config_entry.data) + data[CONF_MAC] = [] + hass.config_entries.async_update_entry(config_entry, data=data) + _LOGGER.info("Migrated config entry to version %d", config_entry.version) return True @@ -584,7 +630,7 @@ class HuaweiLteBaseEntity(Entity): @property def unique_id(self) -> str: """Return unique ID for entity.""" - return f"{self.router.mac}-{self._device_unique_id}" + return f"{self.router.config_entry.unique_id}-{self._device_unique_id}" @property def name(self) -> str: diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 556ed6f5b43..85cfd00d7aa 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,7 +33,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + router = hass.data[DOMAIN].routers[config_entry.unique_id] entities: list[Entity] = [] if router.data.get(KEY_MONITORING_STATUS): diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 3a0a0c32404..a3e7390802f 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -7,7 +7,7 @@ from urllib.parse import urlparse from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client -from huawei_lte_api.Connection import Connection +from huawei_lte_api.Connection import GetResponseType from huawei_lte_api.exceptions import ( LoginErrorPasswordWrongException, LoginErrorUsernamePasswordOverrunException, @@ -22,6 +22,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import ( + CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_RECIPIENT, @@ -34,12 +35,15 @@ from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_TRACK_WIRED_CLIENTS, + CONF_UNAUTHENTICATED_MODE, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME, DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_UNAUTHENTICATED_MODE, DOMAIN, ) +from .utils import get_device_macs _LOGGER = logging.getLogger(__name__) @@ -47,7 +51,7 @@ _LOGGER = logging.getLogger(__name__) class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Huawei LTE config flow.""" - VERSION = 2 + VERSION = 3 @staticmethod @callback @@ -76,10 +80,10 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ): str, vol.Optional( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + CONF_USERNAME, default=user_input.get(CONF_USERNAME) or "" ): str, vol.Optional( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) or "" ): str, } ), @@ -92,15 +96,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle import initiated config flow.""" return await self.async_step_user(user_input) - def _already_configured(self, user_input: dict[str, Any]) -> bool: - """See if we already have a router matching user input configured.""" - existing_urls = { - url_normalize(entry.data[CONF_URL], default_scheme="http") - for entry in self._async_current_entries() - } - return user_input[CONF_URL] in existing_urls - - async def async_step_user( # noqa: C901 + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle user initiated config flow.""" @@ -119,68 +115,46 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input=user_input, errors=errors ) - if self._already_configured(user_input): - return self.async_abort(reason="already_configured") - - conn: Connection | None = None + conn: AuthorizedConnection def logout() -> None: - if isinstance(conn, AuthorizedConnection): - try: - conn.user.logout() - except Exception: # pylint: disable=broad-except - _LOGGER.debug("Could not logout", exc_info=True) + try: + conn.user.logout() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not logout", exc_info=True) - def try_connect(user_input: dict[str, Any]) -> Connection: + def try_connect(user_input: dict[str, Any]) -> AuthorizedConnection: """Try connecting with given credentials.""" - username = user_input.get(CONF_USERNAME) - password = user_input.get(CONF_PASSWORD) - conn: Connection - if username or password: - conn = AuthorizedConnection( - user_input[CONF_URL], - username=username, - password=password, - timeout=CONNECTION_TIMEOUT, - ) - else: - try: - conn = AuthorizedConnection( - user_input[CONF_URL], - username="", - password="", - timeout=CONNECTION_TIMEOUT, - ) - user_input[CONF_USERNAME] = "" - user_input[CONF_PASSWORD] = "" - except ResponseErrorException: - _LOGGER.debug( - "Could not login with empty credentials, proceeding unauthenticated", - exc_info=True, - ) - conn = Connection(user_input[CONF_URL], timeout=CONNECTION_TIMEOUT) - del user_input[CONF_USERNAME] - del user_input[CONF_PASSWORD] + username = user_input.get(CONF_USERNAME) or "" + password = user_input.get(CONF_PASSWORD) or "" + conn = AuthorizedConnection( + user_input[CONF_URL], + username=username, + password=password, + timeout=CONNECTION_TIMEOUT, + ) return conn - def get_router_title(conn: Connection) -> str: - """Get title for router.""" - title = None + def get_device_info() -> tuple[GetResponseType, GetResponseType]: + """Get router info.""" client = Client(conn) try: - info = client.device.basic_information() + device_info = client.device.information() except Exception: # pylint: disable=broad-except - _LOGGER.debug("Could not get device.basic_information", exc_info=True) - else: - title = info.get("devicename") - if not title: + _LOGGER.debug("Could not get device.information", exc_info=True) try: - info = client.device.information() + device_info = client.device.basic_information() except Exception: # pylint: disable=broad-except - _LOGGER.debug("Could not get device.information", exc_info=True) - else: - title = info.get("DeviceName") - return title or DEFAULT_DEVICE_NAME + _LOGGER.debug( + "Could not get device.basic_information", exc_info=True + ) + device_info = {} + try: + wlan_settings = client.wlan.multi_basic_settings() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not get wlan.multi_basic_settings", exc_info=True) + wlan_settings = {} + return device_info, wlan_settings try: conn = await self.hass.async_add_executor_job(try_connect, user_input) @@ -207,11 +181,25 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input=user_input, errors=errors ) - title = self.context.get("title_placeholders", {}).get( - CONF_NAME - ) or await self.hass.async_add_executor_job(get_router_title, conn) + info, wlan_settings = await self.hass.async_add_executor_job(get_device_info) await self.hass.async_add_executor_job(logout) + if not self.unique_id: + if serial_number := info.get("SerialNumber"): + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + else: + await self._async_handle_discovery_without_unique_id() + + user_input[CONF_MAC] = get_device_macs(info, wlan_settings) + + title = ( + self.context.get("title_placeholders", {}).get(CONF_NAME) + or info.get("DeviceName") # device.information + or info.get("devicename") # device.basic_information + or DEFAULT_DEVICE_NAME + ) + return self.async_create_entry(title=title, data=user_input) async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: @@ -224,21 +212,20 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if "mobile" not in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower(): return self.async_abort(reason="not_huawei_lte") - url = self.context[CONF_URL] = url_normalize( + url = url_normalize( discovery_info.get( ssdp.ATTR_UPNP_PRESENTATION_URL, f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", ) ) - if any( - url == flow["context"].get(CONF_URL) for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") + if serial_number := discovery_info.get(ssdp.ATTR_UPNP_SERIAL): + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + else: + await self._async_handle_discovery_without_unique_id() user_input = {CONF_URL: url} - if self._already_configured(user_input): - return self.async_abort(reason="already_configured") self.context["title_placeholders"] = { CONF_NAME: discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) @@ -289,6 +276,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS ), ): bool, + vol.Optional( + CONF_UNAUTHENTICATED_MODE, + default=self.config_entry.options.get( + CONF_UNAUTHENTICATED_MODE, DEFAULT_UNAUTHENTICATED_MODE + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 7e34b3dbd16..b9cbf546087 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,11 +2,15 @@ DOMAIN = "huawei_lte" +ATTR_UNIQUE_ID = "unique_id" + CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" +CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode" DEFAULT_DEVICE_NAME = "LTE" DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN DEFAULT_TRACK_WIRED_CLIENTS = True +DEFAULT_UNAUTHENTICATED_MODE = False UPDATE_SIGNAL = f"{DOMAIN}_update" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 61d2bf30fb9..5c451f71545 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -14,7 +14,6 @@ from homeassistant.components.device_tracker.const import ( SOURCE_TYPE_ROUTER, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -61,7 +60,7 @@ async def async_setup_entry( # Grab hosts list once to examine whether the initial fetch has got some data for # us, i.e. if wlan host list is supported. Only set up a subscription and proceed # with adding and tracking entities if it is. - router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + router = hass.data[DOMAIN].routers[config_entry.unique_id] if (hosts := _get_hosts(router, True)) is None: return @@ -94,10 +93,10 @@ async def async_setup_entry( router.subscriptions[KEY_LAN_HOST_INFO].add(_DEVICE_SCAN) router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) - async def _async_maybe_add_new_entities(url: str) -> None: + async def _async_maybe_add_new_entities(unique_id: str) -> None: """Add new entities if the update signal comes from our router.""" - if url == router.url: - async_add_new_entities(hass, url, async_add_entities, tracked) + if config_entry.unique_id == unique_id: + async_add_new_entities(router, async_add_entities, tracked) # Register to handle router data updates disconnect_dispatcher = async_dispatcher_connect( @@ -106,7 +105,7 @@ async def async_setup_entry( config_entry.async_on_unload(disconnect_dispatcher) # Add new entities from initial scan - async_add_new_entities(hass, router.url, async_add_entities, tracked) + async_add_new_entities(router, async_add_entities, tracked) def _is_wireless(host: _HostType) -> bool: @@ -129,13 +128,11 @@ def _is_us(host: _HostType) -> bool: @callback def async_add_new_entities( - hass: HomeAssistant, - router_url: str, + router: Router, async_add_entities: AddEntitiesCallback, tracked: set[str], ) -> None: """Add new entities that are not already being tracked.""" - router = hass.data[DOMAIN].routers[router_url] hosts = _get_hosts(router) if not hosts: return diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 5d3c15f634a..9cfc008921b 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "getmac==0.8.2", "huawei-lte-api==1.4.18", "stringcase==1.2.0", "url-normalize==1.4.1" diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 1b3b85b6711..fab19427637 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -9,11 +9,11 @@ import attr from huawei_lte_api.exceptions import ResponseErrorException from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService -from homeassistant.const import CONF_RECIPIENT, CONF_URL +from homeassistant.const import CONF_RECIPIENT from homeassistant.core import HomeAssistant from . import Router -from .const import DOMAIN +from .const import ATTR_UNIQUE_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,7 @@ async def async_get_service( if discovery_info is None: return None - router = hass.data[DOMAIN].routers[discovery_info[CONF_URL]] + router = hass.data[DOMAIN].routers[discovery_info[ATTR_UNIQUE_ID]] default_targets = discovery_info[CONF_RECIPIENT] or [] return HuaweiLteSmsNotificationService(router, default_targets) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 7396502793e..6554e69d76e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_URL, DATA_BYTES, DATA_RATE_BYTES_PER_SECOND, PERCENTAGE, @@ -360,7 +359,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + router = hass.data[DOMAIN].routers[config_entry.unique_id] sensors: list[Entity] = [] for key in SENSOR_KEYS: if not (items := router.data.get(key)): diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 9cfa49604ae..0c1373192c5 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "not_huawei_lte": "Not a Huawei LTE device" }, "error": { @@ -23,7 +21,7 @@ "url": "[%key:common::config_flow::data::url%]", "username": "[%key:common::config_flow::data::username%]" }, - "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", + "description": "Enter device access details.", "title": "Configure Huawei LTE" } } @@ -34,7 +32,8 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", - "track_wired_clients": "Track wired network clients" + "track_wired_clients": "Track wired network clients", + "unauthenticated_mode": "Unauthenticated mode (change requires reload)" } } } diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index ff4109943bc..a4fd393346c 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -12,7 +12,6 @@ from homeassistant.components.switch import ( SwitchEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -29,7 +28,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + router = hass.data[DOMAIN].routers[config_entry.unique_id] switches: list[Entity] = [] if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py new file mode 100644 index 00000000000..69b346a58f4 --- /dev/null +++ b/homeassistant/components/huawei_lte/utils.py @@ -0,0 +1,23 @@ +"""Utilities for the Huawei LTE integration.""" +from __future__ import annotations + +from huawei_lte_api.Connection import GetResponseType + +from homeassistant.helpers.device_registry import format_mac + + +def get_device_macs( + device_info: GetResponseType, wlan_settings: GetResponseType +) -> list[str]: + """Get list of device MAC addresses. + + :param device_info: the device.information structure for the device + :param wlan_settings: the wlan.multi_basic_settings structure for the device + """ + macs = [device_info.get("MacAddress1"), device_info.get("MacAddress2")] + try: + macs.extend(x.get("WifiMac") for x in wlan_settings["Ssids"]["Ssid"]) + except Exception: # pylint: disable=broad-except + # Assume not supported + pass + return sorted({format_mac(str(x)) for x in macs if x}) diff --git a/requirements_all.txt b/requirements_all.txt index 5773b2684f5..ee7a4b9565f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,7 +670,6 @@ georss_ign_sismologia_client==0.3 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 -# homeassistant.components.huawei_lte # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70a42e76a80..dad5c4ddc41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,6 @@ georss_ign_sismologia_client==0.3 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 -# homeassistant.components.huawei_lte # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index baffcef3476..4eb9557c6a7 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -10,7 +10,7 @@ from requests_mock import ANY from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.huawei_lte.const import CONF_UNAUTHENTICATED_MODE, DOMAIN from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -21,6 +21,8 @@ from homeassistant.const import ( from tests.common import MockConfigEntry +FIXTURE_UNIQUE_ID = "SERIALNUMBER" + FIXTURE_USER_INPUT = { CONF_URL: "http://192.168.1.1/", CONF_USERNAME: "admin", @@ -57,20 +59,30 @@ async def test_urlize_plain_host(hass, requests_mock): assert user_input[CONF_URL] == f"http://{host}/" -async def test_already_configured(hass): +async def test_already_configured(hass, requests_mock, login_requests_mock): """Test we reject already configured devices.""" MockConfigEntry( - domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured" + domain=DOMAIN, + unique_id=FIXTURE_UNIQUE_ID, + data=FIXTURE_USER_INPUT, + title="Already configured", ).add_to_hass(hass) + login_requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + text="OK", + ) + requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/information", + text=f"{FIXTURE_UNIQUE_ID}", + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data={ - **FIXTURE_USER_INPUT, - # Tweak URL a bit to check that doesn't fail duplicate detection - CONF_URL: FIXTURE_USER_INPUT[CONF_URL].replace("http", "HTTP"), - }, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -182,7 +194,7 @@ async def test_ssdp(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert context[CONF_URL] == url + assert result["data_schema"]({})[CONF_URL] == url async def test_options(hass): @@ -203,3 +215,4 @@ async def test_options(hass): ) assert result["data"][CONF_NAME] == DOMAIN assert result["data"][CONF_RECIPIENT] == [recipient] + assert result["data"][CONF_UNAUTHENTICATED_MODE] is False From 2ecfd74fa48ee6db74491f465914c4552c6892db Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 12 Jul 2021 07:58:45 +0200 Subject: [PATCH 202/818] Add more data_types to modbus (#52423) * Add more data_types. * Use new struct when writing temperature. --- .coveragerc | 1 + homeassistant/components/modbus/__init__.py | 19 +++ homeassistant/components/modbus/climate.py | 4 +- homeassistant/components/modbus/const.py | 22 +++- homeassistant/components/modbus/validators.py | 120 +++++++++++------- tests/components/modbus/test_init.py | 3 +- tests/components/modbus/test_sensor.py | 55 +------- 7 files changed, 123 insertions(+), 101 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3c7f7640623..d7eae65becf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -634,6 +634,7 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/modbus.py + homeassistant/components/modbus/validators.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index e60bbbda78b..8ccfe45f86c 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -94,9 +94,18 @@ from .const import ( CONF_WRITE_TYPE, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, + DATA_TYPE_FLOAT16, + DATA_TYPE_FLOAT32, + DATA_TYPE_FLOAT64, DATA_TYPE_INT, + DATA_TYPE_INT16, + DATA_TYPE_INT32, + DATA_TYPE_INT64, DATA_TYPE_STRING, DATA_TYPE_UINT, + DATA_TYPE_UINT16, + DATA_TYPE_UINT32, + DATA_TYPE_UINT64, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, @@ -137,6 +146,16 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( vol.Optional(CONF_COUNT, default=1): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( [ + DATA_TYPE_INT16, + DATA_TYPE_INT32, + DATA_TYPE_INT64, + DATA_TYPE_UINT16, + DATA_TYPE_UINT32, + DATA_TYPE_UINT64, + DATA_TYPE_FLOAT16, + DATA_TYPE_FLOAT32, + DATA_TYPE_FLOAT64, + DATA_TYPE_STRING, DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 5c99ac86d6c..bfc04024e45 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -40,6 +40,7 @@ from .const import ( CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, + DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -156,7 +157,8 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale ) byte_string = struct.pack(self._structure, target_temperature) - register_value = struct.unpack(">h", byte_string[0:2])[0] + struct_string = f">{DEFAULT_STRUCT_FORMAT[self._data_type]}" + register_value = struct.unpack(struct_string, byte_string)[0] result = await self._hub.async_pymodbus_call( self._slave, self._target_temperature_register, diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 8fb4626d2fe..2607fd2bdf0 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -79,6 +79,15 @@ DATA_TYPE_FLOAT = "float" DATA_TYPE_INT = "int" DATA_TYPE_UINT = "uint" DATA_TYPE_STRING = "string" +DATA_TYPE_INT16 = "int16" +DATA_TYPE_INT32 = "int32" +DATA_TYPE_INT64 = "int64" +DATA_TYPE_UINT16 = "uint16" +DATA_TYPE_UINT32 = "uint32" +DATA_TYPE_UINT64 = "uint64" +DATA_TYPE_FLOAT16 = "float16" +DATA_TYPE_FLOAT32 = "float32" +DATA_TYPE_FLOAT64 = "float64" # call types CALL_TYPE_COIL = "coil" @@ -100,9 +109,16 @@ DEFAULT_SCAN_INTERVAL = 15 # seconds DEFAULT_SLAVE = 1 DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_STRUCT_FORMAT = { - DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}, - DATA_TYPE_UINT: {1: "H", 2: "I", 4: "Q"}, - DATA_TYPE_FLOAT: {1: "e", 2: "f", 4: "d"}, + DATA_TYPE_INT16: "h", + DATA_TYPE_INT32: "i", + DATA_TYPE_INT64: "q", + DATA_TYPE_UINT16: "H", + DATA_TYPE_UINT32: "I", + DATA_TYPE_UINT64: "Q", + DATA_TYPE_FLOAT16: "e", + DATA_TYPE_FLOAT32: "f", + DATA_TYPE_FLOAT64: "d", + DATA_TYPE_STRING: "s", } DEFAULT_TEMP_UNIT = "C" MODBUS_DOMAIN = "modbus" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 03f27dd461b..6e8b740c415 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -21,7 +21,18 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_NONE, DATA_TYPE_CUSTOM, - DATA_TYPE_STRING, + DATA_TYPE_FLOAT, + DATA_TYPE_FLOAT16, + DATA_TYPE_FLOAT32, + DATA_TYPE_FLOAT64, + DATA_TYPE_INT, + DATA_TYPE_INT16, + DATA_TYPE_INT32, + DATA_TYPE_INT64, + DATA_TYPE_UINT, + DATA_TYPE_UINT16, + DATA_TYPE_UINT32, + DATA_TYPE_UINT64, DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCT_FORMAT, PLATFORMS, @@ -29,58 +40,79 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +old_data_types = { + DATA_TYPE_INT: { + 1: DATA_TYPE_INT16, + 2: DATA_TYPE_INT32, + 4: DATA_TYPE_INT64, + }, + DATA_TYPE_UINT: { + 1: DATA_TYPE_UINT16, + 2: DATA_TYPE_UINT32, + 4: DATA_TYPE_UINT64, + }, + DATA_TYPE_FLOAT: { + 1: DATA_TYPE_FLOAT16, + 2: DATA_TYPE_FLOAT32, + 4: DATA_TYPE_FLOAT64, + }, +} + def sensor_schema_validator(config): """Sensor schema validator.""" - if config[CONF_DATA_TYPE] == DATA_TYPE_STRING: - structure = str(config[CONF_COUNT] * 2) + "s" - elif config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: - try: - structure = ( - f">{DEFAULT_STRUCT_FORMAT[config[CONF_DATA_TYPE]][config[CONF_COUNT]]}" - ) - except KeyError as key: - raise vol.Invalid( - f"Unable to detect data type for {config[CONF_NAME]} sensor, try a custom type" - ) from key - else: - structure = config.get(CONF_STRUCTURE) - - if not structure: - raise vol.Invalid( - f"Error in sensor {config[CONF_NAME]}. The `{CONF_STRUCTURE}` field can not be empty " - f"if the parameter `{CONF_DATA_TYPE}` is set to the `{DATA_TYPE_CUSTOM}`" - ) - - try: - size = struct.calcsize(structure) - except struct.error as err: - raise vol.Invalid( - f"Error in sensor {config[CONF_NAME]} structure: {str(err)}" - ) from err - - bytecount = config[CONF_COUNT] * 2 - if bytecount != size: - raise vol.Invalid( - f"Structure request {size} bytes, " - f"but {config[CONF_COUNT]} registers have a size of {bytecount} bytes" - ) - + data_type = config[CONF_DATA_TYPE] + count = config[CONF_COUNT] + name = config[CONF_NAME] + structure = config.get(CONF_STRUCTURE) swap_type = config.get(CONF_SWAP) + if data_type in [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]: + error = f"{name} {name} with {data_type} is not valid, trying to convert" + _LOGGER.warning(error) + try: + data_type = old_data_types[data_type][count] + except KeyError as exp: + raise vol.Invalid("cannot convert automatically") from exp - if config.get(CONF_SWAP) != CONF_SWAP_NONE: - if swap_type == CONF_SWAP_BYTE: - regs_needed = 1 - else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD - regs_needed = 2 - if config[CONF_COUNT] < regs_needed or (config[CONF_COUNT] % regs_needed) != 0: + if config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: + try: + structure = f">{DEFAULT_STRUCT_FORMAT[data_type]}" + except KeyError: raise vol.Invalid( - f"Error in sensor {config[CONF_NAME]} swap({swap_type}) " - f"not possible due to the registers " - f"count: {config[CONF_COUNT]}, needed: {regs_needed}" + f"Modbus error {data_type} unknown in {name}" + ) from KeyError + else: + if not structure: + raise vol.Invalid( + f"Error in sensor {config[CONF_NAME]}. The `{CONF_STRUCTURE}` field can not be empty " + f"if the parameter `{CONF_DATA_TYPE}` is set to the `{DATA_TYPE_CUSTOM}`" ) + try: + size = struct.calcsize(structure) + except struct.error as err: + raise vol.Invalid(f"Error in {name} structure: {str(err)}") from err + + bytecount = count * 2 + if bytecount != size: + raise vol.Invalid( + f"Structure request {size} bytes, " + f"but {count} registers have a size of {bytecount} bytes" + ) + + if swap_type != CONF_SWAP_NONE: + if swap_type == CONF_SWAP_BYTE: + regs_needed = 1 + else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD + regs_needed = 2 + if count < regs_needed or (count % regs_needed) != 0: + raise vol.Invalid( + f"Error in sensor {name} swap({swap_type}) " + f"not possible due to the registers " + f"count: {count}, needed: {regs_needed}" + ) + return { **config, CONF_STRUCTURE: structure, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 435b8446b6b..7dda164e5eb 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -180,7 +180,8 @@ async def test_ok_sensor_schema_validator(do_config): { CONF_NAME: TEST_SENSOR_NAME, CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">f", CONF_SWAP: CONF_SWAP_WORD, }, ], diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index f9bc8454281..a3433a504b8 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -135,15 +135,6 @@ async def test_config_sensor(hass, mock_modbus): @pytest.mark.parametrize( "do_config,error_message", [ - ( - { - CONF_ADDRESS: 1234, - CONF_COUNT: 8, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, - }, - "Unable to detect data type for test_sensor sensor, try a custom type", - ), ( { CONF_ADDRESS: 1234, @@ -152,7 +143,7 @@ async def test_config_sensor(hass, mock_modbus): CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: ">no struct", }, - "Error in sensor test_sensor structure: bad char in struct format", + "bad char in struct format", ), ( { @@ -172,7 +163,7 @@ async def test_config_sensor(hass, mock_modbus): CONF_SWAP: CONF_SWAP_NONE, CONF_STRUCTURE: "invalid", }, - "Error in sensor test_sensor structure: bad char in struct format", + "bad char in struct format", ), ( { @@ -229,7 +220,7 @@ async def test_config_wrong_struct_sensor( expect_setup_to_fail=True, ) - assert error_message in caplog.text + assert caplog.text.count(error_message) @pytest.mark.parametrize( @@ -597,46 +588,6 @@ async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state -@pytest.mark.parametrize( - "swap_type, error_message", - [ - ( - CONF_SWAP_WORD, - f"Error in sensor {SENSOR_NAME} swap(word) not possible due to the registers count: 1, needed: 2", - ), - ( - CONF_SWAP_WORD_BYTE, - f"Error in sensor {SENSOR_NAME} swap(word_byte) not possible due to the registers count: 1, needed: 2", - ), - ], -) -async def test_swap_sensor_wrong_config( - hass, caplog, swap_type, error_message, mock_pymodbus -): - """Run test for sensor swap.""" - config = { - CONF_NAME: SENSOR_NAME, - CONF_ADDRESS: 1234, - CONF_COUNT: 1, - CONF_SWAP: swap_type, - CONF_DATA_TYPE: DATA_TYPE_INT, - } - - caplog.set_level(logging.ERROR) - caplog.clear() - await base_config_test( - hass, - config, - SENSOR_NAME, - SENSOR_DOMAIN, - CONF_SENSORS, - None, - method_discovery=True, - expect_setup_to_fail=True, - ) - assert error_message in "".join(caplog.messages) - - async def test_service_sensor_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" config = { From fd6b5ed072db2c1ca17a3e16a49f3ec4a4a4e47c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 12 Jul 2021 08:17:50 +0200 Subject: [PATCH 203/818] Prefer using xy over hs when supported by light (#52883) --- homeassistant/components/deconz/light.py | 8 ++++-- tests/components/deconz/test_light.py | 34 +++++++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 90d5e82af71..058147189e6 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -26,6 +26,7 @@ from homeassistant.components.light import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.color import color_hs_to_xy from .const import ( COVER_TYPES, @@ -189,8 +190,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + if COLOR_MODE_XY in self._attr_supported_color_modes: + data["xy"] = color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + else: + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) if ATTR_XY_COLOR in kwargs: data["xy"] = kwargs[ATTR_XY_COLOR] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 2beb339dd4f..42dc04fc7ae 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -371,6 +371,34 @@ async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket): @pytest.mark.parametrize( "input,expected", [ + ( # Turn on light with hue and sat + { + "light_on": True, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_HS_COLOR: (20, 30), + }, + }, + { + "on": True, + "xy": (0.411, 0.351), + }, + ), + ( # Turn on light with XY color + { + "light_on": True, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_XY_COLOR: (0.411, 0.351), + }, + }, + { + "on": True, + "xy": (0.411, 0.351), + }, + ), ( # Turn on light with short color loop { "light_on": False, @@ -811,9 +839,8 @@ async def test_groups(hass, aioclient_mock, input, expected): }, }, { - "hue": 45510, "on": True, - "sat": 127, + "xy": (0.235, 0.164), }, ), ( # Turn on group with short color loop @@ -827,9 +854,8 @@ async def test_groups(hass, aioclient_mock, input, expected): }, }, { - "hue": 45510, "on": True, - "sat": 127, + "xy": (0.235, 0.164), }, ), ], From 13c142a4027eba929aa013f4b9644cfe5c3e9f2f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 12 Jul 2021 08:58:53 +0200 Subject: [PATCH 204/818] Fix Soundbar exclusion from SamsungTV (#51023) * Improved check * Fix tests * Fix logic and tests * Update tests --- .../components/samsungtv/config_flow.py | 8 ++ .../components/samsungtv/test_config_flow.py | 86 +++++++++++-------- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 76128a1f1dd..392beda6ac5 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -134,6 +134,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, self._bridge, self._host ) if not info: + if not _method: + LOGGER.debug( + "Samsung host %s is not supported by either %s or %s methods", + self._host, + METHOD_LEGACY, + METHOD_WEBSOCKET, + ) + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) return False dev_info = info.get("device", {}) device_type = dev_info.get("type") diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index a0d2875ca59..502b4f0ced8 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -20,6 +20,7 @@ from homeassistant.components.samsungtv.const import ( RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, + RESULT_SUCCESS, RESULT_UNKNOWN_HOST, TIMEOUT_REQUEST, TIMEOUT_WEBSOCKET, @@ -110,6 +111,14 @@ MOCK_WS_ENTRY = { CONF_MODEL: "any", CONF_NAME: "any", } +MOCK_DEVICE_INFO = { + "device": { + "type": "Samsung SmartTV", + "name": "fake_name", + "modelName": "fake_model", + }, + "id": "123", +} AUTODETECT_LEGACY = { "name": "HomeAssistant", @@ -306,17 +315,22 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): assert result["type"] == "form" assert result["step_id"] == "confirm" - # entry was added - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "create_entry" - assert result["title"] == "fake2_model" - assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "fake2_model" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" - assert result["data"][CONF_MODEL] == "fake2_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.try_connect", + return_value=RESULT_SUCCESS, + ): + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake2_model" + assert result["data"][CONF_HOST] == "fake2_host" + assert result["data"][CONF_NAME] == "fake2_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" + assert result["data"][CONF_MODEL] == "fake2_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): @@ -334,27 +348,32 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): assert result["step_id"] == "confirm" # missing authentication - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "abort" - assert result["reason"] == RESULT_AUTH_MISSING + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.try_connect", + return_value=RESULT_AUTH_MISSING, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_AUTH_MISSING async def test_ssdp_legacy_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery for not supported device.""" + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=UnhandledResponse("Boom"), + "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.try_connect", + return_value=RESULT_NOT_SUPPORTED, ): - - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" - # device not supported result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" @@ -395,17 +414,10 @@ async def test_ssdp_websocket_not_supported(hass: HomeAssistant, remote: Mock): "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), ): - # confirm to add the entry + # device not supported result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" - - # device not supported - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) assert result["type"] == "abort" assert result["reason"] == RESULT_NOT_SUPPORTED @@ -431,6 +443,9 @@ async def test_ssdp_not_successful(hass: HomeAssistant, remote: Mock): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=MOCK_DEVICE_INFO, ): # confirm to add the entry @@ -456,6 +471,9 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant, remote: Mock): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + return_value=MOCK_DEVICE_INFO, ): # confirm to add the entry From 12555d09d6dcff86545ba07736e9e8cb4260b58b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 03:48:42 -0400 Subject: [PATCH 205/818] Use entity class attributes for Blinksticklight (#52892) * Use entity class attributes for blinksticklight * rework * remove self._serial --- .../components/blinksticklight/light.py | 46 +++++-------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index d1120420756..a45eadc3d3a 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -42,57 +42,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkStickLight(LightEntity): """Representation of a BlinkStick light.""" + _attr_supported_features = SUPPORT_BLINKSTICK + def __init__(self, stick, name): """Initialize the light.""" self._stick = stick - self._name = name - self._serial = stick.get_serial() - self._hs_color = None - self._brightness = None - - @property - def name(self): - """Return the name of the light.""" - return self._name - - @property - def brightness(self): - """Read back the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Read back the color of the light.""" - return self._hs_color - - @property - def is_on(self): - """Return True if entity is on.""" - return self._brightness > 0 - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BLINKSTICK + self._attr_name = name def update(self): """Read back the device state.""" rgb_color = self._stick.get_color() hsv = color_util.color_RGB_to_hsv(*rgb_color) - self._hs_color = hsv[:2] - self._brightness = hsv[2] + self._attr_hs_color = hsv[:2] + self._attr_brightness = hsv[2] + self._attr_is_on = self.brightness > 0 def turn_on(self, **kwargs): """Turn the device on.""" if ATTR_HS_COLOR in kwargs: - self._hs_color = kwargs[ATTR_HS_COLOR] + self._attr_hs_color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] else: - self._brightness = 255 + self._attr_brightness = 255 + self._attr_is_on = self.brightness > 0 rgb_color = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 + self.hs_color[0], self.hs_color[1], self.brightness / 255 * 100 ) self._stick.set_color(red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) From a1d6e12c4583b071e5872efcb1819db44bab1346 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 04:20:15 -0400 Subject: [PATCH 206/818] Use entity class attributes for Bh1750 (#52886) * Use entity class attributes for bh1750 * rework --- homeassistant/components/bh1750/sensor.py | 34 +++++------------------ 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 5b708ae2630..8a1f8c60ccf 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -96,42 +96,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BH1750Sensor(SensorEntity): """Implementation of the BH1750 sensor.""" + _attr_device_class = DEVICE_CLASS_ILLUMINANCE + def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): """Initialize the sensor.""" - self._name = name - self._unit_of_measurement = unit + self._attr_name = name + self._attr_unit_of_measurement = unit self._multiplier = multiplier self.bh1750_sensor = bh1750_sensor - if self.bh1750_sensor.light_level >= 0: - self._state = int(round(self.bh1750_sensor.light_level)) - else: - self._state = None - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def state(self) -> int: - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of the sensor.""" - return self._unit_of_measurement - - @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_ILLUMINANCE async def async_update(self): """Get the latest data from the BH1750 and update the states.""" await self.hass.async_add_executor_job(self.bh1750_sensor.update) if self.bh1750_sensor.sample_ok and self.bh1750_sensor.light_level >= 0: - self._state = int(round(self.bh1750_sensor.light_level * self._multiplier)) + self._attr_state = int( + round(self.bh1750_sensor.light_level * self._multiplier) + ) else: _LOGGER.warning( "Bad Update of sensor.%s: %s", self.name, self.bh1750_sensor.light_level From 5d2e5d261252215a852fa72ec8f40bab418f6976 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:35:17 +0200 Subject: [PATCH 207/818] Import Protocol from typing (#52848) --- homeassistant/helpers/entity_platform.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5436a01648e..778b7f3747d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -8,9 +8,8 @@ from datetime import datetime, timedelta import logging from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Protocol -from typing_extensions import Protocol import voluptuous as vol from homeassistant import config_entries From 01afd141c61471dff5b8acf2b18f0edd72d43b68 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 04:37:14 -0400 Subject: [PATCH 208/818] Use entity class attributes for Bizkaibus (#52888) --- homeassistant/components/bizkaibus/sensor.py | 28 +++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index d0cade31a72..16f247693af 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -31,40 +31,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None): route = config[CONF_ROUTE] data = Bizkaibus(stop, route) - add_entities([BizkaibusSensor(data, stop, route, name)], True) + add_entities([BizkaibusSensor(data, name)], True) class BizkaibusSensor(SensorEntity): """The class for handling the data.""" - def __init__(self, data, stop, route, name): + _attr_unit_of_measurement = TIME_MINUTES + + def __init__(self, data, name): """Initialize the sensor.""" self.data = data - self.stop = stop - self.route = route - self._name = name - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return TIME_MINUTES + self._attr_name = name def update(self): """Get the latest data from the webservice.""" self.data.update() with suppress(TypeError): - self._state = self.data.info[0][ATTR_DUE_IN] + self._attr_state = self.data.info[0][ATTR_DUE_IN] class Bizkaibus: From 96e78631fcaaf0dc775cccd0ca51efeee8fd7557 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:44:35 +0200 Subject: [PATCH 209/818] Bump dessant/lock-threads from 2.0.3 to 2.1.1 (#52899) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 3059dc5e2ef..62c7299c2b8 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2.0.3 + - uses: dessant/lock-threads@v2.1.1 with: github-token: ${{ github.token }} issue-lock-inactive-days: "30" From 900eab5a68ce78eb9bd07d10cb70aaa873fb872c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 12 Jul 2021 15:57:26 +0200 Subject: [PATCH 210/818] Surepetcare, fix set_lock_state (#52912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/surepetcare/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 8f0c2311518..e9a2c5b73a1 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -158,10 +158,10 @@ class SurePetcareAPI: # https://github.com/PyCQA/pylint/issues/2062 # pylint: disable=no-member if state == LockState.UNLOCKED.name.lower(): - await self.surepy.unlock(flap_id) + await self.surepy.sac.unlock(flap_id) elif state == LockState.LOCKED_IN.name.lower(): - await self.surepy.lock_in(flap_id) + await self.surepy.sac.lock_in(flap_id) elif state == LockState.LOCKED_OUT.name.lower(): - await self.surepy.lock_out(flap_id) + await self.surepy.sac.lock_out(flap_id) elif state == LockState.LOCKED_ALL.name.lower(): - await self.surepy.lock(flap_id) + await self.surepy.sac.lock(flap_id) From 11edbcabc8697f54ceb54be86500401b773a48bc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 10:03:27 -0400 Subject: [PATCH 211/818] Use entity class attributes for Bitcoin (#52887) * Use entity class attributes for bitcoin * rework * fix * tweak --- homeassistant/components/bitcoin/sensor.py | 77 ++++++++-------------- 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 4acce03d6fa..d11c2a2b726 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -79,39 +79,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BitcoinSensor(SensorEntity): """Representation of a Bitcoin sensor.""" + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_icon = ICON + def __init__(self, data, option_type, currency): """Initialize the sensor.""" self.data = data - self._name = OPTION_TYPES[option_type][0] - self._unit_of_measurement = OPTION_TYPES[option_type][1] + self._attr_name = OPTION_TYPES[option_type][0] + self._attr_unit_of_measurement = OPTION_TYPES[option_type][1] self._currency = currency self.type = option_type - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest data and updates the states.""" @@ -120,48 +97,48 @@ class BitcoinSensor(SensorEntity): ticker = self.data.ticker if self.type == "exchangerate": - self._state = ticker[self._currency].p15min - self._unit_of_measurement = self._currency + self._attr_state = ticker[self._currency].p15min + self._attr_unit_of_measurement = self._currency elif self.type == "trade_volume_btc": - self._state = f"{stats.trade_volume_btc:.1f}" + self._attr_state = f"{stats.trade_volume_btc:.1f}" elif self.type == "miners_revenue_usd": - self._state = f"{stats.miners_revenue_usd:.0f}" + self._attr_state = f"{stats.miners_revenue_usd:.0f}" elif self.type == "btc_mined": - self._state = str(stats.btc_mined * 0.00000001) + self._attr_state = str(stats.btc_mined * 0.00000001) elif self.type == "trade_volume_usd": - self._state = f"{stats.trade_volume_usd:.1f}" + self._attr_state = f"{stats.trade_volume_usd:.1f}" elif self.type == "difficulty": - self._state = f"{stats.difficulty:.0f}" + self._attr_state = f"{stats.difficulty:.0f}" elif self.type == "minutes_between_blocks": - self._state = f"{stats.minutes_between_blocks:.2f}" + self._attr_state = f"{stats.minutes_between_blocks:.2f}" elif self.type == "number_of_transactions": - self._state = str(stats.number_of_transactions) + self._attr_state = str(stats.number_of_transactions) elif self.type == "hash_rate": - self._state = f"{stats.hash_rate * 0.000001:.1f}" + self._attr_state = f"{stats.hash_rate * 0.000001:.1f}" elif self.type == "timestamp": - self._state = stats.timestamp + self._attr_state = stats.timestamp elif self.type == "mined_blocks": - self._state = str(stats.mined_blocks) + self._attr_state = str(stats.mined_blocks) elif self.type == "blocks_size": - self._state = f"{stats.blocks_size:.1f}" + self._attr_state = f"{stats.blocks_size:.1f}" elif self.type == "total_fees_btc": - self._state = f"{stats.total_fees_btc * 0.00000001:.2f}" + self._attr_state = f"{stats.total_fees_btc * 0.00000001:.2f}" elif self.type == "total_btc_sent": - self._state = f"{stats.total_btc_sent * 0.00000001:.2f}" + self._attr_state = f"{stats.total_btc_sent * 0.00000001:.2f}" elif self.type == "estimated_btc_sent": - self._state = f"{stats.estimated_btc_sent * 0.00000001:.2f}" + self._attr_state = f"{stats.estimated_btc_sent * 0.00000001:.2f}" elif self.type == "total_btc": - self._state = f"{stats.total_btc * 0.00000001:.2f}" + self._attr_state = f"{stats.total_btc * 0.00000001:.2f}" elif self.type == "total_blocks": - self._state = f"{stats.total_blocks:.0f}" + self._attr_state = f"{stats.total_blocks:.0f}" elif self.type == "next_retarget": - self._state = f"{stats.next_retarget:.2f}" + self._attr_state = f"{stats.next_retarget:.2f}" elif self.type == "estimated_transaction_volume_usd": - self._state = f"{stats.estimated_transaction_volume_usd:.2f}" + self._attr_state = f"{stats.estimated_transaction_volume_usd:.2f}" elif self.type == "miners_revenue_btc": - self._state = f"{stats.miners_revenue_btc * 0.00000001:.1f}" + self._attr_state = f"{stats.miners_revenue_btc * 0.00000001:.1f}" elif self.type == "market_price_usd": - self._state = f"{stats.market_price_usd:.2f}" + self._attr_state = f"{stats.market_price_usd:.2f}" class BitcoinData: From a810c1ff087dadce2f21ad9464bba3bca601ff4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 04:13:07 -1000 Subject: [PATCH 212/818] Bump aiohomekit to 0.5.1 to solve performance regression (#52878) - Changelog: https://github.com/Jc2k/aiohomekit/compare/0.5.0...0.5.1 - Note that #52759 will need to be cherry-picked under this commit --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 816ec2db4d9..4bc61f53cc0 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.5.0"], + "requirements": ["aiohomekit==0.5.1"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index ee7a4b9565f..64473042f2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.5.0 +aiohomekit==0.5.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dad5c4ddc41..b38a2b4fd18 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.5.0 +aiohomekit==0.5.1 # homeassistant.components.emulated_hue # homeassistant.components.http From 0f6a0f6bcdde52a5f312fb2e5e85dc977003f79f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 04:30:54 -1000 Subject: [PATCH 213/818] Add the Trane brand to nexia (#52805) --- homeassistant/components/nexia/config_flow.py | 20 +++++++++++++++---- homeassistant/components/nexia/const.py | 3 ++- homeassistant/components/nexia/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index 18c20a8f92a..4e48123a5de 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Nexia integration.""" import logging -from nexia.const import BRAND_ASAIR, BRAND_NEXIA +from nexia.const import BRAND_ASAIR, BRAND_NEXIA, BRAND_TRANE from nexia.home import NexiaHome from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -9,7 +9,13 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import BRAND_ASAIR_NAME, BRAND_NEXIA_NAME, CONF_BRAND, DOMAIN +from .const import ( + BRAND_ASAIR_NAME, + BRAND_NEXIA_NAME, + BRAND_TRANE_NAME, + CONF_BRAND, + DOMAIN, +) from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) @@ -19,7 +25,11 @@ DATA_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Required(CONF_BRAND, default=BRAND_NEXIA): vol.In( - {BRAND_NEXIA: BRAND_NEXIA_NAME, BRAND_ASAIR: BRAND_ASAIR_NAME} + { + BRAND_NEXIA: BRAND_NEXIA_NAME, + BRAND_ASAIR: BRAND_ASAIR_NAME, + BRAND_TRANE: BRAND_TRANE_NAME, + } ), } ) @@ -31,7 +41,9 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - state_file = hass.config.path(f"nexia_config_{data[CONF_USERNAME]}.conf") + state_file = hass.config.path( + f"{data[CONF_BRAND]}_config_{data[CONF_USERNAME]}.conf" + ) try: nexia_home = NexiaHome( username=data[CONF_USERNAME], diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index d6e3e5f8008..22b24c3b764 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -33,4 +33,5 @@ SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE" SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE" BRAND_NEXIA_NAME = "Nexia" -BRAND_ASAIR_NAME = "American Standard" +BRAND_ASAIR_NAME = "American Standard Home" +BRAND_TRANE_NAME = "Trane Home" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index ed1247ee9e3..eb471597ec6 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", - "name": "Nexia/American Standard", - "requirements": ["nexia==0.9.7"], + "name": "Nexia/American Standard/Trane", + "requirements": ["nexia==0.9.9"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 64473042f2b..2c5f5995925 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1018,7 +1018,7 @@ nettigo-air-monitor==1.0.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.7 +nexia==0.9.9 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b38a2b4fd18..5cf667419a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -572,7 +572,7 @@ netdisco==2.9.0 nettigo-air-monitor==1.0.0 # homeassistant.components.nexia -nexia==0.9.7 +nexia==0.9.9 # homeassistant.components.notify_events notify-events==1.0.4 From 9308fa28e715da559b197aa4593d6fdcc71443f5 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 12 Jul 2021 15:46:54 +0100 Subject: [PATCH 214/818] Improve testing of option flow in Coinbase (#52870) --- tests/components/coinbase/const.py | 7 -- tests/components/coinbase/test_config_flow.py | 72 +++-------------- tests/components/coinbase/test_init.py | 80 +++++++++++++++++++ 3 files changed, 91 insertions(+), 68 deletions(-) diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 864ebc18701..7d36d0be9a7 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -9,13 +9,6 @@ BAD_CURRENCY = "ETH" BAD_EXCHANGE_RATE = "ETH" MOCK_ACCOUNTS_RESPONSE = [ - { - "balance": {"amount": "13.38", "currency": GOOD_CURRENCY_3}, - "currency": GOOD_CURRENCY_3, - "id": "ABCDEF", - "name": "BTC Wallet", - "native_balance": {"amount": "15.02", "currency": GOOD_CURRENCY_2}, - }, { "balance": {"amount": "0.00001", "currency": GOOD_CURRENCY}, "currency": GOOD_CURRENCY, diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index 4c7b6c13333..d153cecc249 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -19,14 +19,7 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) -from .const import ( - BAD_CURRENCY, - BAD_EXCHANGE_RATE, - GOOD_CURRENCY, - GOOD_CURRENCY_2, - GOOD_EXCHNAGE_RATE, - GOOD_EXCHNAGE_RATE_2, -) +from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE from tests.common import MockConfigEntry @@ -144,19 +137,8 @@ async def test_form_catch_all_exception(hass): assert result2["errors"] == {"base": "unknown"} -async def test_option_good_account_currency(hass): +async def test_option_form(hass): """Test we handle a good wallet currency option.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - title="Test User", - data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, - options={ - CONF_CURRENCIES: [GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [], - }, - ) - config_entry.add_to_hass(hass) with patch( "coinbase.wallet.client.Client.get_current_user", @@ -166,8 +148,11 @@ async def test_option_good_account_currency(hass): ), patch( "coinbase.wallet.client.Client.get_exchange_rates", return_value=mock_get_exchange_rates(), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) + ), patch( + "homeassistant.components.coinbase.update_listener" + ) as mock_update_listener: + + config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() @@ -175,10 +160,12 @@ async def test_option_good_account_currency(hass): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY], - CONF_EXCHANGE_RATES: [], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], }, ) - assert result2["type"] == "create_entry" + assert result2["type"] == "create_entry" + await hass.async_block_till_done() + assert len(mock_update_listener.mock_calls) == 1 async def test_form_bad_account_currency(hass): @@ -207,43 +194,6 @@ async def test_form_bad_account_currency(hass): assert result2["errors"] == {"base": "currency_unavaliable"} -async def test_option_good_exchange_rate(hass): - """Test we handle a good exchange rate option.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - title="Test User", - data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, - options={ - CONF_CURRENCIES: [], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE_2], - }, - ) - config_entry.add_to_hass(hass) - - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_CURRENCIES: [], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], - }, - ) - assert result2["type"] == "create_entry" - - async def test_form_bad_exchange_rate(hass): """Test we handle a bad exchange rate.""" with patch( diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index 612519b1cee..36f0ff95472 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -9,6 +9,8 @@ from homeassistant.components.coinbase.const import ( DOMAIN, ) from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from .common import ( @@ -78,3 +80,81 @@ async def test_unload_entry(hass): assert entry.state == config_entries.ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_option_updates(hass: HomeAssistant): + """Test handling option updates.""" + + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + }, + ) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entities = entity_registry.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + assert len(entities) == 4 + currencies = [ + entity.unique_id.split("-")[-1] + for entity in entities + if "wallet" in entity.unique_id + ] + + rates = [ + entity.unique_id.split("-")[-1] + for entity in entities + if "xe" in entity.unique_id + ] + + assert currencies == [GOOD_CURRENCY, GOOD_CURRENCY_2] + assert rates == [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2] + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [GOOD_CURRENCY], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], + }, + ) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entities = entity_registry.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + assert len(entities) == 2 + currencies = [ + entity.unique_id.split("-")[-1] + for entity in entities + if "wallet" in entity.unique_id + ] + + rates = [ + entity.unique_id.split("-")[-1] + for entity in entities + if "xe" in entity.unique_id + ] + + assert currencies == [GOOD_CURRENCY] + assert rates == [GOOD_EXCHNAGE_RATE] From 2b6a3716e81598bb97d11413e5af14a98b3567a8 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 12 Jul 2021 10:09:45 -0500 Subject: [PATCH 215/818] Ignore Sonos Boost devices during discovery (#52845) --- homeassistant/components/sonos/__init__.py | 4 ++++ homeassistant/components/sonos/speaker.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index ec16ec5bd87..040ee321206 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -46,6 +46,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" +DISCOVERY_IGNORED_MODELS = ["Sonos Boost"] CONFIG_SCHEMA = vol.Schema( @@ -233,6 +234,9 @@ async def async_setup_entry( # noqa: C901 @callback def _async_discovered_player(info): + if info.get("modelName") in DISCOVERY_IGNORED_MODELS: + _LOGGER.debug("Ignoring device: %s", info.get("friendlyName")) + return uid = info.get(ssdp.ATTR_UPNP_UDN) if uid.startswith("uuid:"): uid = uid[5:] diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 14adbc337fb..b1483c0f5d3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -511,7 +511,7 @@ class SonosSpeaker: await self.async_unsubscribe() if not will_reconnect: - self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid) + self.hass.data[DATA_SONOS].ssdp_known.discard(self.soco.uid) self.async_write_entity_states() async def async_rebooted(self, soco: SoCo) -> None: From 40549d9d2f37de071a4fd541603e8a8ce5e19f8c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 12 Jul 2021 17:24:56 +0200 Subject: [PATCH 216/818] Add some type hints for webhook component (#52895) * Add some type hints * Fix type hint * Address comment * Make pylint happy --- homeassistant/components/webhook/__init__.py | 22 ++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 6d61f5d62dc..8331722c397 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,6 +1,10 @@ """Webhooks for Home Assistant.""" +from __future__ import annotations + +from collections.abc import Awaitable import logging import secrets +from typing import Callable from aiohttp.web import Request, Response import voluptuous as vol @@ -8,7 +12,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import HTTP_OK -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest @@ -28,7 +32,13 @@ SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @callback @bind_hass -def async_register(hass, domain, name, webhook_id, handler): +def async_register( + hass: HomeAssistant, + domain: str, + name: str, + webhook_id: str, + handler: Callable[[HomeAssistant, str, Request], Awaitable[Response | None]], +) -> None: """Register a webhook.""" handlers = hass.data.setdefault(DOMAIN, {}) @@ -40,21 +50,21 @@ def async_register(hass, domain, name, webhook_id, handler): @callback @bind_hass -def async_unregister(hass, webhook_id): +def async_unregister(hass: HomeAssistant, webhook_id: str) -> None: """Remove a webhook.""" handlers = hass.data.setdefault(DOMAIN, {}) handlers.pop(webhook_id, None) @callback -def async_generate_id(): +def async_generate_id() -> str: """Generate a webhook_id.""" return secrets.token_hex(32) @callback @bind_hass -def async_generate_url(hass, webhook_id): +def async_generate_url(hass: HomeAssistant, webhook_id: str) -> str: """Generate the full URL for a webhook_id.""" return "{}{}".format( get_url(hass, prefer_external=True, allow_cloud=False), @@ -63,7 +73,7 @@ def async_generate_url(hass, webhook_id): @callback -def async_generate_path(webhook_id): +def async_generate_path(webhook_id: str) -> str: """Generate the path component for a webhook_id.""" return URL_WEBHOOK_PATH.format(webhook_id=webhook_id) From 6a5dcf0869621610e03ccc55a93843a5ee0302fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 05:25:16 -1000 Subject: [PATCH 217/818] Handle dhcp packets without a hostname (#52882) * Handle dhcp packets without a hostname - Since some integrations only match on OUI we want to make sure they still see devices that do not request a specific hostname * Update tests/components/dhcp/test_init.py * Update homeassistant/components/dhcp/__init__.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/dhcp/__init__.py | 8 +-- tests/components/dhcp/test_init.py | 64 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 5d0b31c8788..7003038593b 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -248,10 +248,10 @@ class DeviceTrackerWatcher(WatcherBase): return ip_address = attributes.get(ATTR_IP) - hostname = attributes.get(ATTR_HOST_NAME) + hostname = attributes.get(ATTR_HOST_NAME, "") mac_address = attributes.get(ATTR_MAC) - if ip_address is None or hostname is None or mac_address is None: + if ip_address is None or mac_address is None: return self.process_client(ip_address, hostname, _format_mac(mac_address)) @@ -328,10 +328,10 @@ class DHCPWatcher(WatcherBase): return ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src - hostname = _decode_dhcp_option(options, HOSTNAME) + hostname = _decode_dhcp_option(options, HOSTNAME) or "" mac_address = _format_mac(packet[Ether].src) - if ip_address is None or hostname is None or mac_address is None: + if ip_address is None or mac_address is None: return self.process_client(ip_address, hostname, mac_address) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 122e81786c2..0da383c758a 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -81,6 +81,47 @@ RAW_DHCP_RENEWAL = ( b"\x43\x37\x08\x01\x21\x03\x06\x1c\x33\x3a\x3b\xff" ) +# 60:6b:bd:59:e4:b4 192.168.107.151 +RAW_DHCP_REQUEST_WITHOUT_HOSTNAME = ( + b"\xff\xff\xff\xff\xff\xff\x60\x6b\xbd\x59\xe4\xb4\x08\x00\x45\x00" + b"\x02\x40\x00\x00\x00\x00\x40\x11\x78\xae\x00\x00\x00\x00\xff\xff" + b"\xff\xff\x00\x44\x00\x43\x02\x2c\x02\x04\x01\x01\x06\x00\xff\x92" + b"\x7e\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x60\x6b\xbd\x59\xe4\xb4\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x03\x3d\x07\x01" + b"\x60\x6b\xbd\x59\xe4\xb4\x3c\x25\x75\x64\x68\x63\x70\x20\x31\x2e" + b"\x31\x34\x2e\x33\x2d\x56\x44\x20\x4c\x69\x6e\x75\x78\x20\x56\x44" + b"\x4c\x69\x6e\x75\x78\x2e\x31\x2e\x32\x2e\x31\x2e\x78\x32\x04\xc0" + b"\xa8\x6b\x97\x36\x04\xc0\xa8\x6b\x01\x37\x07\x01\x03\x06\x0c\x0f" + b"\x1c\x2a\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + async def test_dhcp_match_hostname_and_macaddress(hass): """Test matching based on hostname and macaddress.""" @@ -182,6 +223,29 @@ async def test_dhcp_match_macaddress(hass): } +async def test_dhcp_match_macaddress_without_hostname(hass): + """Test matching based on macaddress only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "macaddress": "606BBD*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST_WITHOUT_HOSTNAME) + + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.107.151", + dhcp.HOSTNAME: "", + dhcp.MAC_ADDRESS: "606bbd59e4b4", + } + + async def test_dhcp_nomatch(hass): """Test not matching based on macaddress only.""" dhcp_watcher = dhcp.DHCPWatcher( From 98109caee9f28292cdc7c5f22f8e5a73ea8c2a11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 06:24:12 -1000 Subject: [PATCH 218/818] Add zeroconf discovery to Sonos (#52655) --- homeassistant/components/sonos/__init__.py | 192 ++++++++++-------- homeassistant/components/sonos/config_flow.py | 48 ++++- homeassistant/components/sonos/const.py | 3 + homeassistant/components/sonos/helpers.py | 19 ++ homeassistant/components/sonos/manifest.json | 3 +- homeassistant/components/sonos/speaker.py | 25 ++- homeassistant/components/sonos/strings.json | 1 + .../components/sonos/translations/en.json | 1 + homeassistant/generated/zeroconf.py | 5 + tests/components/sonos/test_config_flow.py | 92 +++++++++ tests/components/sonos/test_helpers.py | 17 ++ 11 files changed, 315 insertions(+), 91 deletions(-) create mode 100644 tests/components/sonos/test_config_flow.py create mode 100644 tests/components/sonos/test_helpers.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 040ee321206..3d810c7e1a3 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s from .alarms import SonosAlarms from .const import ( DATA_SONOS, + DATA_SONOS_DISCOVERY_MANAGER, DISCOVERY_INTERVAL, DOMAIN, PLATFORMS, @@ -91,7 +92,7 @@ class SonosData: self.alarms: dict[str, SonosAlarms] = {} self.topology_condition = asyncio.Condition() self.hosts_heartbeat = None - self.ssdp_known: set[str] = set() + self.discovery_known: set[str] = set() self.boot_counts: dict[str, int] = {} @@ -111,9 +112,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" pysonos.config.EVENTS_MODULE = events_asyncio @@ -123,7 +122,6 @@ async def async_setup_entry( # noqa: C901 data = hass.data[DATA_SONOS] config = hass.data[DOMAIN].get("media_player", {}) hosts = config.get(CONF_HOSTS, []) - discovery_lock = asyncio.Lock() _LOGGER.debug("Reached async_setup_entry, config=%s", config) advertise_addr = config.get(CONF_ADVERTISE_ADDR) @@ -137,153 +135,181 @@ async def async_setup_entry( # noqa: C901 deprecated_address, ) - async def _async_stop_event_listener(event: Event) -> None: + manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager( + hass, entry, data, hosts + ) + hass.async_create_task(manager.setup_platforms_and_discovery()) + return True + + +def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None: + """Create a soco instance and return if successful.""" + try: + soco = pysonos.SoCo(ip_address) + # Ensure that the player is available and UID is cached + _ = soco.uid + _ = soco.volume + return soco + except (OSError, SoCoException) as ex: + _LOGGER.warning( + "Failed to connect to %s player '%s': %s", source.value, ip_address, ex + ) + return None + + +class SonosDiscoveryManager: + """Manage sonos discovery.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, data: SonosData, hosts: list[str] + ) -> None: + """Init discovery manager.""" + self.hass = hass + self.entry = entry + self.data = data + self.hosts = hosts + self.discovery_lock = asyncio.Lock() + + async def _async_stop_event_listener(self, event: Event) -> None: await asyncio.gather( - *[speaker.async_unsubscribe() for speaker in data.discovered.values()], + *[speaker.async_unsubscribe() for speaker in self.data.discovered.values()], return_exceptions=True, ) if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() - def _stop_manual_heartbeat(event: Event) -> None: - if data.hosts_heartbeat: - data.hosts_heartbeat() - data.hosts_heartbeat = None + def _stop_manual_heartbeat(self, event: Event) -> None: + if self.data.hosts_heartbeat: + self.data.hosts_heartbeat() + self.data.hosts_heartbeat = None - def _discovered_player(soco: SoCo) -> None: + def _discovered_player(self, soco: SoCo) -> None: """Handle a (re)discovered player.""" try: speaker_info = soco.get_speaker_info(True) _LOGGER.debug("Adding new speaker: %s", speaker_info) - speaker = SonosSpeaker(hass, soco, speaker_info) - data.discovered[soco.uid] = speaker + speaker = SonosSpeaker(self.hass, soco, speaker_info) + self.data.discovered[soco.uid] = speaker for coordinator, coord_dict in [ - (SonosAlarms, data.alarms), - (SonosFavorites, data.favorites), + (SonosAlarms, self.data.alarms), + (SonosFavorites, self.data.favorites), ]: if soco.household_id not in coord_dict: - new_coordinator = coordinator(hass, soco.household_id) + new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) coord_dict[soco.household_id] = new_coordinator speaker.setup() except (OSError, SoCoException): _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) - def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None: - """Create a soco instance and return if successful.""" - try: - soco = pysonos.SoCo(ip_address) - # Ensure that the player is available and UID is cached - _ = soco.uid - _ = soco.volume - return soco - except (OSError, SoCoException) as ex: - _LOGGER.warning( - "Failed to connect to %s player '%s': %s", source.value, ip_address, ex - ) - return None - - def _manual_hosts(now: datetime.datetime | None = None) -> None: + def _manual_hosts(self, now: datetime.datetime | None = None) -> None: """Players from network configuration.""" - for host in hosts: + for host in self.hosts: ip_addr = socket.gethostbyname(host) known_uid = next( ( uid - for uid, speaker in data.discovered.items() + for uid, speaker in self.data.discovered.items() if speaker.soco.ip_address == ip_addr ), None, ) if known_uid: - dispatcher_send(hass, f"{SONOS_SEEN}-{known_uid}") + dispatcher_send(self.hass, f"{SONOS_SEEN}-{known_uid}") else: soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED) if soco and soco.is_visible: - _discovered_player(soco) + self._discovered_player(soco) - data.hosts_heartbeat = hass.helpers.event.call_later( - DISCOVERY_INTERVAL.total_seconds(), _manual_hosts + self.data.hosts_heartbeat = self.hass.helpers.event.call_later( + DISCOVERY_INTERVAL.total_seconds(), self._manual_hosts ) @callback - def _async_signal_update_groups(event): - async_dispatcher_send(hass, SONOS_GROUP_UPDATE) + def _async_signal_update_groups(self, _event): + async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE) - def _discovered_ip(ip_address): + def _discovered_ip(self, ip_address): soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED) if soco and soco.is_visible: - _discovered_player(soco) + self._discovered_player(soco) - async def _async_create_discovered_player(uid, discovered_ip, boot_seqnum): + async def _async_create_discovered_player(self, uid, discovered_ip, boot_seqnum): """Only create one player at a time.""" - async with discovery_lock: - if uid not in data.discovered: - await hass.async_add_executor_job(_discovered_ip, discovered_ip) + async with self.discovery_lock: + if uid not in self.data.discovered: + await self.hass.async_add_executor_job( + self._discovered_ip, discovered_ip + ) return - if boot_seqnum and boot_seqnum > data.boot_counts[uid]: - data.boot_counts[uid] = boot_seqnum - if soco := await hass.async_add_executor_job( + if boot_seqnum and boot_seqnum > self.data.boot_counts[uid]: + self.data.boot_counts[uid] = boot_seqnum + if soco := await self.hass.async_add_executor_job( _create_soco, discovered_ip, SoCoCreationSource.REBOOTED ): - async_dispatcher_send(hass, f"{SONOS_REBOOTED}-{uid}", soco) + async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}", soco) else: - async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}") + async_dispatcher_send(self.hass, f"{SONOS_SEEN}-{uid}") @callback - def _async_discovered_player(info): - if info.get("modelName") in DISCOVERY_IGNORED_MODELS: - _LOGGER.debug("Ignoring device: %s", info.get("friendlyName")) - return + def _async_ssdp_discovered_player(self, info): + discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname + boot_seqnum = info.get("X-RINCON-BOOTSEQ") uid = info.get(ssdp.ATTR_UPNP_UDN) if uid.startswith("uuid:"): uid = uid[5:] - if boot_seqnum := info.get("X-RINCON-BOOTSEQ"): - boot_seqnum = int(boot_seqnum) - data.boot_counts.setdefault(uid, boot_seqnum) - if uid not in data.ssdp_known: - _LOGGER.debug("New discovery: %s", info) - data.ssdp_known.add(uid) - discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname - asyncio.create_task( - _async_create_discovered_player(uid, discovered_ip, boot_seqnum) + self.async_discovered_player( + info, discovered_ip, uid, boot_seqnum, info.get("modelName") ) - async def setup_platforms_and_discovery(): + @callback + def async_discovered_player(self, info, discovered_ip, uid, boot_seqnum, model): + """Handle discovery via ssdp or zeroconf.""" + if model in DISCOVERY_IGNORED_MODELS: + _LOGGER.debug("Ignoring device: %s", info) + return + if boot_seqnum: + boot_seqnum = int(boot_seqnum) + self.data.boot_counts.setdefault(uid, boot_seqnum) + if uid not in self.data.discovery_known: + _LOGGER.debug("New discovery uid=%s: %s", uid, info) + self.data.discovery_known.add(uid) + asyncio.create_task( + self._async_create_discovered_player(uid, discovered_ip, boot_seqnum) + ) + + async def setup_platforms_and_discovery(self): + """Set up platforms and discovery.""" await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(entry, platform) + self.hass.config_entries.async_forward_entry_setup(self.entry, platform) for platform in PLATFORMS ] ) - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_signal_update_groups + self.entry.async_on_unload( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._async_signal_update_groups ) ) - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener + self.entry.async_on_unload( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener ) ) _LOGGER.debug("Adding discovery job") - if hosts: - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _stop_manual_heartbeat + if self.hosts: + self.entry.async_on_unload( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat ) ) - await hass.async_add_executor_job(_manual_hosts) + await self.hass.async_add_executor_job(self._manual_hosts) return - entry.async_on_unload( + self.entry.async_on_unload( ssdp.async_register_callback( - hass, _async_discovered_player, {"st": UPNP_ST} + self.hass, self._async_ssdp_discovered_player, {"st": UPNP_ST} ) ) - - hass.async_create_task(setup_platforms_and_discovery()) - - return True diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 5037abb79aa..1ba750c24be 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,10 +1,19 @@ """Config flow for SONOS.""" +import logging + import pysonos +from homeassistant import config_entries +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_flow +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import DOMAIN +from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN +from .helpers import hostname_to_uid + +_LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass: HomeAssistant) -> bool: @@ -13,4 +22,37 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: return bool(result) -config_entry_flow.register_discovery_flow(DOMAIN, "Sonos", _async_has_devices) +class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): + """Sonos discovery flow that callsback zeroconf updates.""" + + def __init__(self) -> None: + """Init discovery flow.""" + super().__init__(DOMAIN, "Sonos", _async_has_devices) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle a flow initialized by zeroconf.""" + hostname = discovery_info["hostname"] + if hostname is None or not hostname.startswith("Sonos-"): + return self.async_abort(reason="not_sonos_device") + await self.async_set_unique_id(self._domain, raise_on_progress=False) + host = discovery_info[CONF_HOST] + properties = discovery_info["properties"] + boot_seqnum = properties.get("bootseq") + model = properties.get("model") + uid = hostname_to_uid(hostname) + _LOGGER.debug( + "Calling async_discovered_player for %s with uid=%s and boot_seqnum=%s", + host, + uid, + boot_seqnum, + ) + if discovery_manager := self.hass.data.get(DATA_SONOS_DISCOVERY_MANAGER): + discovery_manager.async_discovered_player( + properties, host, uid, boot_seqnum, model + ) + return await self.async_step_discovery(discovery_info) + + +config_entries.HANDLERS.register(DOMAIN)(SonosDiscoveryFlowHandler) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 9072f4cab02..aca4b9b39ae 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -26,6 +26,7 @@ UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" +DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager" PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN} SONOS_ARTIST = "artists" @@ -154,3 +155,5 @@ SCAN_INTERVAL = datetime.timedelta(seconds=10) DISCOVERY_INTERVAL = datetime.timedelta(seconds=60) SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL SUBSCRIPTION_TIMEOUT = 1200 + +MDNS_SERVICE = "_sonos._tcp.local." diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index ac8cd00d9db..675a3e8e9f2 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -9,6 +9,9 @@ from pysonos.exceptions import SoCoException, SoCoUPnPException from homeassistant.exceptions import HomeAssistantError +UID_PREFIX = "RINCON_" +UID_POSTFIX = "01400" + _LOGGER = logging.getLogger(__name__) @@ -36,3 +39,19 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable: return wrapper return decorator + + +def uid_to_short_hostname(uid: str) -> str: + """Convert a Sonos uid to a short hostname.""" + hostname_uid = uid + if hostname_uid.startswith(UID_PREFIX): + hostname_uid = hostname_uid[len(UID_PREFIX) :] + if hostname_uid.endswith(UID_POSTFIX): + hostname_uid = hostname_uid[: -len(UID_POSTFIX)] + return f"Sonos-{hostname_uid}" + + +def hostname_to_uid(hostname: str) -> str: + """Convert a Sonos hostname to a uid.""" + baseuid = hostname.split("-")[1].replace(".local.", "") + return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index a3b031ac07b..b1b0bc8a202 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": ["pysonos==0.0.51"], "dependencies": ["ssdp"], - "after_dependencies": ["plex"], + "after_dependencies": ["plex", "zeroconf"], + "zeroconf": ["_sonos._tcp.local."], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b1483c0f5d3..19f65f963c3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -19,6 +19,7 @@ from pysonos.music_library import MusicLibrary from pysonos.plugins.sharelink import ShareLinkPlugin from pysonos.snapshot import Snapshot +from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -37,6 +38,7 @@ from .const import ( BATTERY_SCAN_INTERVAL, DATA_SONOS, DOMAIN, + MDNS_SERVICE, PLATFORMS, SCAN_INTERVAL, SEEN_EXPIRE_TIME, @@ -56,7 +58,7 @@ from .const import ( SUBSCRIPTION_TIMEOUT, ) from .favorites import SonosFavorites -from .helpers import soco_error +from .helpers import soco_error, uid_to_short_hostname EVENT_CHARGING = { "CHARGING": True, @@ -498,12 +500,27 @@ class SonosSpeaker: self, now: datetime.datetime | None = None, will_reconnect: bool = False ) -> None: """Make this player unavailable when it was not seen recently.""" - self._share_link_plugin = None - if self._seen_timer: self._seen_timer() self._seen_timer = None + hostname = uid_to_short_hostname(self.soco.uid) + zcname = f"{hostname}.{MDNS_SERVICE}" + aiozeroconf = await zeroconf.async_get_async_instance(self.hass) + if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname): + # We can still see the speaker via zeroconf check again later. + self._seen_timer = self.hass.helpers.event.async_call_later( + SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen + ) + return + + _LOGGER.debug( + "No activity and could not locate %s on the network. Marking unavailable", + zcname, + ) + + self._share_link_plugin = None + if self._poll_timer: self._poll_timer() self._poll_timer = None @@ -511,7 +528,7 @@ class SonosSpeaker: await self.async_unsubscribe() if not will_reconnect: - self.hass.data[DATA_SONOS].ssdp_known.discard(self.soco.uid) + self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) self.async_write_entity_states() async def async_rebooted(self, soco: SoCo) -> None: diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 12812d66692..fb73e30421f 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -6,6 +6,7 @@ } }, "abort": { + "not_sonos_device": "Discovered device is not a Sonos device", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } diff --git a/homeassistant/components/sonos/translations/en.json b/homeassistant/components/sonos/translations/en.json index 38aecd5e965..181ddc2f5bf 100644 --- a/homeassistant/components/sonos/translations/en.json +++ b/homeassistant/components/sonos/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "No devices found on the network", + "not_sonos_device": "Discovered device is not a Sonos device", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "step": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 11fd47469f8..536485f7f55 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -186,6 +186,11 @@ ZEROCONF = { "name": "brother*" } ], + "_sonos._tcp.local.": [ + { + "domain": "sonos" + } + ], "_spotify-connect._tcp.local.": [ { "domain": "spotify" diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py new file mode 100644 index 00000000000..9dd308ae28f --- /dev/null +++ b/tests/components/sonos/test_config_flow.py @@ -0,0 +1,92 @@ +"""Test the sonos config flow.""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from homeassistant import config_entries, core, setup +from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN + + +@patch("homeassistant.components.sonos.config_flow.pysonos.discover", return_value=True) +async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): + """Test we get the user initiated form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + with patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Sonos" + assert result2["data"] == {} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_form(hass: core.HomeAssistant): + """Test we pass sonos devices to the discovery manager.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "host": "192.168.4.2", + "hostname": "Sonos-aaa", + "properties": {"bootseq": "1234"}, + }, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Sonos" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_manager.mock_calls) == 2 + + +async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): + """Test we abort on non-sonos devices.""" + mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "host": "192.168.4.2", + "hostname": "not-aaa", + "properties": {"bootseq": "1234"}, + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_sonos_device" + assert len(mock_manager.mock_calls) == 0 diff --git a/tests/components/sonos/test_helpers.py b/tests/components/sonos/test_helpers.py new file mode 100644 index 00000000000..858657e01c0 --- /dev/null +++ b/tests/components/sonos/test_helpers.py @@ -0,0 +1,17 @@ +"""Test the sonos config flow.""" +from __future__ import annotations + +from homeassistant.components.sonos.helpers import ( + hostname_to_uid, + uid_to_short_hostname, +) + + +async def test_uid_to_short_hostname(): + """Test we can convert a uid to a short hostname.""" + assert uid_to_short_hostname("RINCON_347E5C0CF1E301400") == "Sonos-347E5C0CF1E3" + + +async def test_uid_to_hostname(): + """Test we can convert a hostname to a uid.""" + assert hostname_to_uid("Sonos-347E5C0CF1E3.local.") == "RINCON_347E5C0CF1E301400" From 8c812bc25c141d687f6d435f48d1b537b4caa5b8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 18:27:11 +0200 Subject: [PATCH 219/818] Improve typing of Tasmota (2/3) (#52747) * Improve typing of Tasmota (2/3) * Add more typing, add TasmotaOnOffEntity * Address review comments --- .../components/tasmota/binary_sensor.py | 51 +++++++++++++----- homeassistant/components/tasmota/cover.py | 54 +++++++++++++------ .../components/tasmota/device_automation.py | 14 +++-- .../components/tasmota/device_trigger.py | 54 ++++++++++++------- homeassistant/components/tasmota/discovery.py | 45 +++++++++++++--- homeassistant/components/tasmota/fan.py | 54 ++++++++++++++----- homeassistant/components/tasmota/light.py | 10 ++-- homeassistant/components/tasmota/mixins.py | 36 ++++++++++--- homeassistant/components/tasmota/sensor.py | 10 +++- homeassistant/components/tasmota/switch.py | 8 +-- 10 files changed, 241 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/tasmota/binary_sensor.py b/homeassistant/components/tasmota/binary_sensor.py index feaafa72b29..1ccee0bf7d3 100644 --- a/homeassistant/components/tasmota/binary_sensor.py +++ b/homeassistant/components/tasmota/binary_sensor.py @@ -1,9 +1,19 @@ """Support for Tasmota binary sensors.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Callable + +from hatasmota import switch as tasmota_switch +from hatasmota.entity import TasmotaEntity as HATasmotaEntity +from hatasmota.models import DiscoveryHashType from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.event as evt from .const import DATA_REMOVE_DISCOVER_COMPONENT @@ -11,11 +21,17 @@ from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Tasmota binary sensor dynamically through discovery.""" @callback - def async_discover(tasmota_entity, discovery_hash): + def async_discover( + tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota binary sensor.""" async_add_entities( [ @@ -41,33 +57,40 @@ class TasmotaBinarySensor( ): """Representation a Tasmota binary sensor.""" - def __init__(self, **kwds): + _tasmota_entity: tasmota_switch.TasmotaSwitch + + def __init__(self, **kwds: Any) -> None: """Initialize the Tasmota binary sensor.""" - self._delay_listener = None - self._state = None + self._delay_listener: Callable | None = None + self._on_off_state: bool | None = None super().__init__( **kwds, ) + async def async_added_to_hass(self) -> None: + """Subscribe to MQTT events.""" + self._tasmota_entity.set_on_state_callback(self.on_off_state_updated) + await super().async_added_to_hass() + @callback - def off_delay_listener(self, now): + def off_delay_listener(self, now: datetime) -> None: """Switch device off after a delay.""" self._delay_listener = None - self._state = False + self._on_off_state = False self.async_write_ha_state() @callback - def state_updated(self, state, **kwargs): + def on_off_state_updated(self, state: bool, **kwargs: Any) -> None: """Handle state updates.""" - self._state = state + self._on_off_state = state if self._delay_listener is not None: self._delay_listener() self._delay_listener = None off_delay = self._tasmota_entity.off_delay - if self._state and off_delay is not None: + if self._on_off_state and off_delay is not None: self._delay_listener = evt.async_call_later( self.hass, off_delay, self.off_delay_listener ) @@ -75,11 +98,11 @@ class TasmotaBinarySensor( self.async_write_ha_state() @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return True @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" - return self._state + return self._on_off_state diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py index 681778d0099..458c712ae3d 100644 --- a/homeassistant/components/tasmota/cover.py +++ b/homeassistant/components/tasmota/cover.py @@ -1,22 +1,35 @@ """Support for Tasmota covers.""" +from __future__ import annotations -from hatasmota import const as tasmota_const +from typing import Any + +from hatasmota import const as tasmota_const, shutter as tasmota_shutter +from hatasmota.entity import TasmotaEntity as HATasmotaEntity +from hatasmota.models import DiscoveryHashType from homeassistant.components import cover from homeassistant.components.cover import CoverEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Tasmota cover dynamically through discovery.""" @callback - def async_discover(tasmota_entity, discovery_hash): + def async_discover( + tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota cover.""" async_add_entities( [TasmotaCover(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] @@ -38,24 +51,31 @@ class TasmotaCover( ): """Representation of a Tasmota cover.""" - def __init__(self, **kwds): + _tasmota_entity: tasmota_shutter.TasmotaShutter + + def __init__(self, **kwds: Any) -> None: """Initialize the Tasmota cover.""" - self._direction = None - self._position = None + self._direction: int | None = None + self._position: int | None = None super().__init__( **kwds, ) + async def async_added_to_hass(self) -> None: + """Subscribe to MQTT events.""" + self._tasmota_entity.set_on_state_callback(self.cover_state_updated) + await super().async_added_to_hass() + @callback - def state_updated(self, state, **kwargs): + def cover_state_updated(self, state: bool, **kwargs: Any) -> None: """Handle state updates.""" self._direction = kwargs["direction"] self._position = kwargs["position"] self.async_write_ha_state() @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. @@ -63,7 +83,7 @@ class TasmotaCover( return self._position @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return ( cover.SUPPORT_OPEN @@ -73,35 +93,35 @@ class TasmotaCover( ) @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._direction == tasmota_const.SHUTTER_DIRECTION_UP @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._direction == tasmota_const.SHUTTER_DIRECTION_DOWN @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if self._position is None: return None return self._position == 0 - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._tasmota_entity.open() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" self._tasmota_entity.close() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[cover.ATTR_POSITION] self._tasmota_entity.set_position(position) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._tasmota_entity.stop() diff --git a/homeassistant/components/tasmota/device_automation.py b/homeassistant/components/tasmota/device_automation.py index ff431141bef..9b190855ad2 100644 --- a/homeassistant/components/tasmota/device_automation.py +++ b/homeassistant/components/tasmota/device_automation.py @@ -1,7 +1,11 @@ """Provides device automations for Tasmota.""" from hatasmota.const import AUTOMATION_TYPE_TRIGGER +from hatasmota.models import DiscoveryHashType +from hatasmota.trigger import TasmotaTrigger +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -10,21 +14,23 @@ from .const import DATA_REMOVE_DISCOVER_COMPONENT, DATA_UNSUB from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW -async def async_remove_automations(hass, device_id): +async def async_remove_automations(hass: HomeAssistant, device_id: str) -> None: """Remove automations for a Tasmota device.""" await device_trigger.async_remove_triggers(hass, device_id) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up Tasmota device automation dynamically through discovery.""" - async def async_device_removed(event): + async def async_device_removed(event: Event) -> None: """Handle the removal of a device.""" if event.data["action"] != "remove": return await async_remove_automations(hass, event.data["device_id"]) - async def async_discover(tasmota_automation, discovery_hash): + async def async_discover( + tasmota_automation: TasmotaTrigger, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota device automation.""" if tasmota_automation.automation_type == AUTOMATION_TYPE_TRIGGER: await device_trigger.async_setup_trigger( diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 8ec368e326f..eb95ca2bf64 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -5,12 +5,14 @@ import logging from typing import Callable import attr -from hatasmota.trigger import TasmotaTrigger +from hatasmota.models import DiscoveryHashType +from hatasmota.trigger import TasmotaTrigger, TasmotaTriggerConfig import voluptuous as vol from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -51,8 +53,9 @@ class TriggerInstance: trigger: Trigger = attr.ib() remove: CALLBACK_TYPE | None = attr.ib(default=None) - async def async_attach_trigger(self): + async def async_attach_trigger(self) -> None: """Attach event trigger.""" + assert self.trigger.tasmota_trigger is not None event_config = { event_trigger.CONF_PLATFORM: "event", event_trigger.CONF_EVENT_TYPE: TASMOTA_EVENT, @@ -81,15 +84,17 @@ class Trigger: """Device trigger settings.""" device_id: str = attr.ib() - discovery_hash: dict | None = attr.ib() + discovery_hash: DiscoveryHashType | None = attr.ib() hass: HomeAssistant = attr.ib() remove_update_signal: Callable[[], None] | None = attr.ib() subtype: str = attr.ib() - tasmota_trigger: TasmotaTrigger = attr.ib() + tasmota_trigger: TasmotaTrigger | None = attr.ib() type: str = attr.ib() trigger_instances: list[TriggerInstance] = attr.ib(factory=list) - async def add_trigger(self, action, automation_info): + async def add_trigger( + self, action: AutomationActionType, automation_info: dict + ) -> Callable[[], None]: """Add Tasmota trigger.""" instance = TriggerInstance(action, automation_info, self) self.trigger_instances.append(instance) @@ -110,7 +115,7 @@ class Trigger: return async_remove - def detach_trigger(self): + def detach_trigger(self) -> None: """Remove Tasmota device trigger.""" # Mark trigger as unknown self.tasmota_trigger = None @@ -121,11 +126,12 @@ class Trigger: trig.remove() trig.remove = None - async def arm_tasmota_trigger(self): + async def arm_tasmota_trigger(self) -> None: """Arm Tasmota trigger: subscribe to MQTT topics and fire events.""" @callback - def _on_trigger(): + def _on_trigger() -> None: + assert self.tasmota_trigger is not None data = { "mac": self.tasmota_trigger.cfg.mac, "source": self.tasmota_trigger.cfg.subtype, @@ -136,10 +142,13 @@ class Trigger: data, ) + assert self.tasmota_trigger is not None self.tasmota_trigger.set_on_trigger_callback(_on_trigger) await self.tasmota_trigger.subscribe_topics() - async def set_tasmota_trigger(self, tasmota_trigger, remove_update_signal): + async def set_tasmota_trigger( + self, tasmota_trigger: TasmotaTrigger, remove_update_signal: Callable[[], None] + ) -> None: """Set Tasmota trigger.""" await self.update_tasmota_trigger(tasmota_trigger.cfg, remove_update_signal) self.tasmota_trigger = tasmota_trigger @@ -147,22 +156,31 @@ class Trigger: for trig in self.trigger_instances: await trig.async_attach_trigger() - async def update_tasmota_trigger(self, tasmota_trigger_cfg, remove_update_signal): + async def update_tasmota_trigger( + self, + tasmota_trigger_cfg: TasmotaTriggerConfig, + remove_update_signal: Callable[[], None], + ) -> None: """Update Tasmota trigger.""" self.remove_update_signal = remove_update_signal self.type = tasmota_trigger_cfg.type self.subtype = tasmota_trigger_cfg.subtype -async def async_setup_trigger(hass, tasmota_trigger, config_entry, discovery_hash): +async def async_setup_trigger( + hass: HomeAssistant, + tasmota_trigger: TasmotaTrigger, + config_entry: ConfigEntry, + discovery_hash: DiscoveryHashType, +) -> None: """Set up a discovered Tasmota device trigger.""" discovery_id = tasmota_trigger.cfg.trigger_id - remove_update_signal = None + remove_update_signal: Callable[[], None] | None = None _LOGGER.debug( "Discovered trigger with ID: %s '%s'", discovery_id, tasmota_trigger.cfg ) - async def discovery_update(trigger_config): + async def discovery_update(trigger_config: TasmotaTriggerConfig) -> None: """Handle discovery update.""" _LOGGER.debug( "Got update for trigger with hash: %s '%s'", discovery_hash, trigger_config @@ -175,7 +193,8 @@ async def async_setup_trigger(hass, tasmota_trigger, config_entry, discovery_has await device_trigger.tasmota_trigger.unsubscribe_topics() device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) - remove_update_signal() + if remove_update_signal is not None: + remove_update_signal() return device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] @@ -226,7 +245,7 @@ async def async_setup_trigger(hass, tasmota_trigger, config_entry, discovery_has await device_trigger.arm_tasmota_trigger() -async def async_remove_triggers(hass: HomeAssistant, device_id: str): +async def async_remove_triggers(hass: HomeAssistant, device_id: str) -> None: """Cleanup any device triggers for a Tasmota device.""" triggers = await async_get_triggers(hass, device_id) for trig in triggers: @@ -287,6 +306,5 @@ async def async_attach_trigger( subtype=config[CONF_SUBTYPE], tasmota_trigger=None, ) - return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( - action, automation_info - ) + trigger: Trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + return await trigger.add_trigger(action, automation_info) diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index ad3604c06c2..1e5bde5a3d5 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -1,5 +1,8 @@ """Support for Tasmota device discovery.""" +from __future__ import annotations + import logging +from typing import Callable from hatasmota.discovery import ( TasmotaDiscovery, @@ -10,8 +13,13 @@ from hatasmota.discovery import ( get_triggers as tasmota_get_triggers, unique_id_from_hash, ) +from hatasmota.entity import TasmotaEntityConfig +from hatasmota.models import DiscoveryHashType, TasmotaDeviceConfig +from hatasmota.mqtt import TasmotaMQTTClient +from hatasmota.sensor import TasmotaBaseSensorConfig import homeassistant.components.sensor as sensor +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dev_reg from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -26,8 +34,12 @@ TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}" TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}" TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance" +SetupDeviceCallback = Callable[[TasmotaDeviceConfig, str], None] -def clear_discovery_hash(hass, discovery_hash): + +def clear_discovery_hash( + hass: HomeAssistant, discovery_hash: DiscoveryHashType +) -> None: """Clear entry in ALREADY_DISCOVERED list.""" if ALREADY_DISCOVERED not in hass.data: # Discovery is shutting down @@ -35,17 +47,25 @@ def clear_discovery_hash(hass, discovery_hash): del hass.data[ALREADY_DISCOVERED][discovery_hash] -def set_discovery_hash(hass, discovery_hash): +def set_discovery_hash(hass: HomeAssistant, discovery_hash: DiscoveryHashType) -> None: """Set entry in ALREADY_DISCOVERED list.""" hass.data[ALREADY_DISCOVERED][discovery_hash] = {} async def async_start( - hass: HomeAssistant, discovery_topic, config_entry, tasmota_mqtt, setup_device + hass: HomeAssistant, + discovery_topic: str, + config_entry: ConfigEntry, + tasmota_mqtt: TasmotaMQTTClient, + setup_device: SetupDeviceCallback, ) -> None: """Start Tasmota device discovery.""" - async def _discover_entity(tasmota_entity_config, discovery_hash, platform): + async def _discover_entity( + tasmota_entity_config: TasmotaEntityConfig | None, + discovery_hash: DiscoveryHashType, + platform: str, + ) -> None: """Handle adding or updating a discovered entity.""" if not tasmota_entity_config: # Entity disabled, clean up entity registry @@ -70,6 +90,10 @@ async def async_start( ) else: tasmota_entity = tasmota_get_entity(tasmota_entity_config, tasmota_mqtt) + if not tasmota_entity: + _LOGGER.error("Failed to create entity %s %s", platform, discovery_hash) + return + _LOGGER.debug( "Adding new entity: %s %s %s", platform, @@ -86,7 +110,7 @@ async def async_start( discovery_hash, ) - async def async_device_discovered(payload, mac): + async def async_device_discovered(payload: dict, mac: str) -> None: """Process the received message.""" if ALREADY_DISCOVERED not in hass.data: @@ -102,7 +126,12 @@ async def async_start( tasmota_triggers = tasmota_get_triggers(payload) for trigger_config in tasmota_triggers: - discovery_hash = (mac, "automation", "trigger", trigger_config.trigger_id) + discovery_hash: DiscoveryHashType = ( + mac, + "automation", + "trigger", + trigger_config.trigger_id, + ) if discovery_hash in hass.data[ALREADY_DISCOVERED]: _LOGGER.debug( "Trigger already added, sending update: %s", @@ -131,7 +160,9 @@ async def async_start( for (tasmota_entity_config, discovery_hash) in tasmota_entities: await _discover_entity(tasmota_entity_config, discovery_hash, platform) - async def async_sensors_discovered(sensors, mac): + async def async_sensors_discovered( + sensors: list[tuple[TasmotaBaseSensorConfig, DiscoveryHashType]], mac: str + ) -> None: """Handle discovery of (additional) sensors.""" platform = sensor.DOMAIN diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 876d1a4cf60..92399fa1bbc 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -1,11 +1,18 @@ """Support for Tasmota fans.""" +from __future__ import annotations -from hatasmota import const as tasmota_const +from typing import Any + +from hatasmota import const as tasmota_const, fan as tasmota_fan +from hatasmota.entity import TasmotaEntity as HATasmotaEntity +from hatasmota.models import DiscoveryHashType from homeassistant.components import fan from homeassistant.components.fan import FanEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, @@ -22,11 +29,17 @@ ORDERED_NAMED_FAN_SPEEDS = [ ] # off is not included -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Tasmota fan dynamically through discovery.""" @callback - def async_discover(tasmota_entity, discovery_hash): + def async_discover( + tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota fan.""" async_add_entities( [TasmotaFan(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] @@ -48,21 +61,34 @@ class TasmotaFan( ): """Representation of a Tasmota fan.""" - def __init__(self, **kwds): + _tasmota_entity: tasmota_fan.TasmotaFan + + def __init__(self, **kwds: Any) -> None: """Initialize the Tasmota fan.""" - self._state = None + self._state: int | None = None super().__init__( **kwds, ) + async def async_added_to_hass(self) -> None: + """Subscribe to MQTT events.""" + self._tasmota_entity.set_on_state_callback(self.fan_state_updated) + await super().async_added_to_hass() + + @callback + def fan_state_updated(self, state: int, **kwargs: Any) -> None: + """Handle state updates.""" + self._state = state + self.async_write_ha_state() + @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return len(ORDERED_NAMED_FAN_SPEEDS) @property - def percentage(self): + def percentage(self) -> int | None: """Return the current speed percentage.""" if self._state is None: return None @@ -71,11 +97,11 @@ class TasmotaFan( return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, self._state) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return fan.SUPPORT_SET_SPEED - async def async_set_percentage(self, percentage): + async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan.""" if percentage == 0: await self.async_turn_off() @@ -86,8 +112,12 @@ class TasmotaFan( self._tasmota_entity.set_speed(tasmota_speed) async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ): + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: """Turn the fan on.""" # Tasmota does not support turning a fan on with implicit speed await self.async_set_percentage( @@ -97,6 +127,6 @@ class TasmotaFan( ) ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" self._tasmota_entity.set_speed(tasmota_const.FAN_SPEED_OFF) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 9af95049f79..675a3d175c3 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -30,7 +30,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW -from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate +from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate, TasmotaOnOffEntity DEFAULT_BRIGHTNESS_MAX = 255 TASMOTA_BRIGHTNESS_MAX = 100 @@ -74,6 +74,7 @@ def scale_brightness(brightness): class TasmotaLight( TasmotaAvailability, TasmotaDiscoveryUpdate, + TasmotaOnOffEntity, LightEntity, ): """Representation of a Tasmota light.""" @@ -142,7 +143,7 @@ class TasmotaLight( @callback def state_updated(self, state, **kwargs): """Handle state updates.""" - self._state = state + self._on_off_state = state attributes = kwargs.get("attributes") if attributes: if "brightness" in attributes: @@ -222,11 +223,6 @@ class TasmotaLight( """Force update.""" return False - @property - def is_on(self): - """Return true if device is on.""" - return self._state - @property def supported_color_modes(self): """Flag supported color modes.""" diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index d8e0eeeb4cd..f1b0554957e 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -1,5 +1,8 @@ """Tasmota entity mixins.""" +from __future__ import annotations + import logging +from typing import Any from homeassistant.components.mqtt import ( async_subscribe_connection_status, @@ -24,13 +27,11 @@ class TasmotaEntity(Entity): def __init__(self, tasmota_entity) -> None: """Initialize.""" - self._state = None self._tasmota_entity = tasmota_entity self._unique_id = tasmota_entity.unique_id async def async_added_to_hass(self): """Subscribe to MQTT events.""" - self._tasmota_entity.set_on_state_callback(self.state_updated) await self._subscribe_topics() async def async_will_remove_from_hass(self): @@ -49,12 +50,6 @@ class TasmotaEntity(Entity): """(Re)Subscribe to topics.""" await self._tasmota_entity.subscribe_topics() - @callback - def state_updated(self, state, **kwargs): - """Handle state updates.""" - self._state = state - self.async_write_ha_state() - @property def device_info(self): """Return a device description for device registry.""" @@ -76,6 +71,31 @@ class TasmotaEntity(Entity): return self._unique_id +class TasmotaOnOffEntity(TasmotaEntity): + """Base class for Tasmota entities which can be on or off.""" + + def __init__(self, **kwds: Any) -> None: + """Initialize.""" + self._on_off_state: bool = False + super().__init__(**kwds) + + async def async_added_to_hass(self) -> None: + """Subscribe to MQTT events.""" + self._tasmota_entity.set_on_state_callback(self.state_updated) + await super().async_added_to_hass() + + @callback + def state_updated(self, state: bool, **kwargs: Any) -> None: + """Handle state updates.""" + self._on_off_state = state + self.async_write_ha_state() + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._on_off_state + + class TasmotaAvailability(TasmotaEntity): """Mixin used for platforms that report availability.""" diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index e346f8f13ac..fa4a8270cc7 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from hatasmota import const as hc, status_sensor +from hatasmota import const as hc, sensor as tasmota_sensor, status_sensor from homeassistant.components import sensor from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity @@ -176,6 +176,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): """Representation of a Tasmota sensor.""" _attr_last_reset = None + _tasmota_entity: tasmota_sensor.TasmotaSensor def __init__(self, **kwds): """Initialize the Tasmota sensor.""" @@ -185,8 +186,13 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): **kwds, ) + async def async_added_to_hass(self) -> None: + """Subscribe to MQTT events.""" + self._tasmota_entity.set_on_state_callback(self.sensor_state_updated) + await super().async_added_to_hass() + @callback - def state_updated(self, state, **kwargs): + def sensor_state_updated(self, state, **kwargs): """Handle state updates.""" self._state = state if "last_reset" in kwargs: diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py index 27906bf5dbb..f7fa67bed22 100644 --- a/homeassistant/components/tasmota/switch.py +++ b/homeassistant/components/tasmota/switch.py @@ -7,7 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW -from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate +from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate, TasmotaOnOffEntity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -36,6 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TasmotaSwitch( TasmotaAvailability, TasmotaDiscoveryUpdate, + TasmotaOnOffEntity, SwitchEntity, ): """Representation of a Tasmota switch.""" @@ -48,11 +49,6 @@ class TasmotaSwitch( **kwds, ) - @property - def is_on(self): - """Return true if device is on.""" - return self._state - async def async_turn_on(self, **kwargs): """Turn the device on.""" self._tasmota_entity.set_state(True) From ad0ccc1b709a7747c499683b9bafd860c445f6cd Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 12:29:34 -0400 Subject: [PATCH 220/818] Use entity class attributes for apple_tv (#52664) * Use entity class attributes for apple_tv * fix pylint * tweak --- homeassistant/components/apple_tv/__init__.py | 35 +++++-------------- .../components/apple_tv/media_player.py | 7 ++-- homeassistant/components/apple_tv/remote.py | 7 +--- 3 files changed, 12 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index e8b900d8213..9a96f62dbd1 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -83,12 +83,17 @@ async def async_unload_entry(hass, entry): class AppleTVEntity(Entity): """Device that sends commands to an Apple TV.""" + _attr_should_poll = False + def __init__(self, name, identifier, manager): """Initialize device.""" self.atv = None self.manager = manager - self._name = name - self._identifier = identifier + self._attr_name = name + self._attr_unique_id = identifier + self._attr_device_info = { + "identifiers": {(DOMAIN, identifier)}, + } async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" @@ -109,13 +114,13 @@ class AppleTVEntity(Entity): self.async_on_remove( async_dispatcher_connect( - self.hass, f"{SIGNAL_CONNECTED}_{self._identifier}", _async_connected + self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{SIGNAL_DISCONNECTED}_{self._identifier}", + f"{SIGNAL_DISCONNECTED}_{self.unique_id}", _async_disconnected, ) ) @@ -126,28 +131,6 @@ class AppleTVEntity(Entity): def async_device_disconnected(self): """Handle when connection was lost to device.""" - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._identifier - - @property - def should_poll(self): - """No polling needed for Apple TV.""" - return False - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._identifier)}, - } - class AppleTVManager: """Connection and power manager for an Apple TV. diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index a855fc6b53e..09ecc01015c 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -75,6 +75,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): """Representation of an Apple TV media player.""" + _attr_supported_features = SUPPORT_APPLE_TV + def __init__(self, name, identifier, manager, **kwargs): """Initialize the Apple TV media player.""" super().__init__(name, identifier, manager, **kwargs) @@ -229,11 +231,6 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self._playing.shuffle != ShuffleState.Off return None - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_APPLE_TV - def _is_feature_available(self, feature): """Return if a feature is available.""" if self.atv and self._playing: diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 3d88bddcbc9..853ea29fcf5 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -34,11 +34,6 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): """Return true if device is on.""" return self.atv is not None - @property - def should_poll(self): - """No polling needed for Apple TV.""" - return False - async def async_turn_on(self, **kwargs): """Turn the device on.""" await self.manager.connect() @@ -53,7 +48,7 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) if not self.is_on: - _LOGGER.error("Unable to send commands, not connected to %s", self._name) + _LOGGER.error("Unable to send commands, not connected to %s", self.name) return for _ in range(num_repeats): From 1a74fd7a14494d6a920e22476a3787e2ffd8e1b3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 18:53:52 +0200 Subject: [PATCH 221/818] Set device_class on temperature sensors R-Z (#52920) --- .../components/rainmachine/sensor.py | 8 +++- homeassistant/components/repetier/__init__.py | 23 ++++++--- homeassistant/components/repetier/sensor.py | 1 + homeassistant/components/sensehat/sensor.py | 8 ++-- homeassistant/components/sht31/sensor.py | 3 ++ homeassistant/components/skybeacon/sensor.py | 2 + .../components/solaredge_local/sensor.py | 27 +++++++++-- homeassistant/components/tellstick/sensor.py | 28 +++++++---- homeassistant/components/xbee/sensor.py | 3 +- homeassistant/components/zamg/sensor.py | 47 ++++++++++++++----- homeassistant/components/zwave/sensor.py | 9 +++- 11 files changed, 123 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 87feb85046d..808c6a06bc2 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -5,7 +5,11 @@ from regenmaschine.controller import Controller from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + VOLUME_CUBIC_METERS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -62,7 +66,7 @@ SENSORS = { "Freeze Protect Temperature", "mdi:thermometer", TEMP_CELSIUS, - "temperature", + DEVICE_CLASS_TEMPERATURE, True, DATA_RESTRICTIONS_UNIVERSAL, ), diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index c104fc447e2..08306396e96 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_SENSORS, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -110,23 +111,31 @@ def has_all_unique_names(value): SENSOR_TYPES = { # Type, Unit, Icon, post - "bed_temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer", "_bed_"], + "bed_temperature": [ + "temperature", + TEMP_CELSIUS, + None, + "_bed_", + DEVICE_CLASS_TEMPERATURE, + ], "extruder_temperature": [ "temperature", TEMP_CELSIUS, - "mdi:thermometer", + None, "_extruder_", + DEVICE_CLASS_TEMPERATURE, ], "chamber_temperature": [ "temperature", TEMP_CELSIUS, - "mdi:thermometer", + None, "_chamber_", + DEVICE_CLASS_TEMPERATURE, ], - "current_state": ["state", None, "mdi:printer-3d", ""], - "current_job": ["progress", PERCENTAGE, "mdi:file-percent", "_current_job"], - "job_end": ["progress", None, "mdi:clock-end", "_job_end"], - "job_start": ["progress", None, "mdi:clock-start", "_job_start"], + "current_state": ["state", None, "mdi:printer-3d", "", None], + "current_job": ["progress", PERCENTAGE, "mdi:file-percent", "_current_job", None], + "job_end": ["progress", None, "mdi:clock-end", "_job_end", None], + "job_start": ["progress", None, "mdi:clock-start", "_job_start", None], } SENSOR_SCHEMA = vol.Schema( diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 77a3c51e9cf..46818095647 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -59,6 +59,7 @@ class RepetierSensor(SensorEntity): self._printer_id = printer_id self._sensor_type = sensor_type self._state = None + self._attr_device_class = SENSOR_TYPES[self._sensor_type][4] @property def available(self) -> bool: diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 6ba00baae77..379301b0fa7 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -24,9 +25,9 @@ CONF_IS_HAT_ATTACHED = "is_hat_attached" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { - "temperature": ["temperature", TEMP_CELSIUS], - "humidity": ["humidity", PERCENTAGE], - "pressure": ["pressure", "mb"], + "temperature": ["temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], + "humidity": ["humidity", PERCENTAGE, None], + "pressure": ["pressure", "mb", None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -77,6 +78,7 @@ class SenseHatSensor(SensorEntity): self._unit_of_measurement = SENSOR_TYPES[sensor_types][1] self.type = sensor_types self._state = None + self._attr_device_class = SENSOR_TYPES[sensor_types][2] @property def name(self): diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 65ebbf0d882..a894623db47 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRECISION_TENTHS, TEMP_CELSIUS, @@ -119,6 +120,8 @@ class SHTSensor(SensorEntity): class SHTSensorTemperature(SHTSensor): """Representation of a temperature sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE + @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index fd707f9dd96..5b6eae96a7e 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MAC, CONF_NAME, + DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, STATE_UNKNOWN, @@ -90,6 +91,7 @@ class SkybeaconHumid(SensorEntity): class SkybeaconTemp(SensorEntity): """Representation of a Skybeacon temperature sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_unit_of_measurement = TEMP_CELSIUS def __init__(self, name, mon): diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 441a1c39e08..920cbb564f8 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, + DEVICE_CLASS_TEMPERATURE, ELECTRICAL_CURRENT_AMPERE, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, @@ -51,6 +52,7 @@ SENSOR_TYPES = { FREQUENCY_HERTZ, "mdi:current-ac", None, + None, ], "current_power": [ "currentPower", @@ -58,6 +60,7 @@ SENSOR_TYPES = { POWER_WATT, "mdi:solar-power", None, + None, ], "energy_this_month": [ "energyThisMonth", @@ -65,6 +68,7 @@ SENSOR_TYPES = { ENERGY_WATT_HOUR, "mdi:solar-power", None, + None, ], "energy_this_year": [ "energyThisYear", @@ -72,6 +76,7 @@ SENSOR_TYPES = { ENERGY_WATT_HOUR, "mdi:solar-power", None, + None, ], "energy_today": [ "energyToday", @@ -79,13 +84,15 @@ SENSOR_TYPES = { ENERGY_WATT_HOUR, "mdi:solar-power", None, + None, ], "inverter_temperature": [ "invertertemperature", "Inverter Temperature", TEMP_CELSIUS, - "mdi:thermometer", + None, "operating_mode", + DEVICE_CLASS_TEMPERATURE, ], "lifetime_energy": [ "energyTotal", @@ -93,6 +100,7 @@ SENSOR_TYPES = { ENERGY_WATT_HOUR, "mdi:solar-power", None, + None, ], "optimizer_connected": [ "optimizers", @@ -100,6 +108,7 @@ SENSOR_TYPES = { "optimizers", "mdi:solar-panel", "optimizers_connected", + None, ], "optimizer_current": [ "optimizercurrent", @@ -107,6 +116,7 @@ SENSOR_TYPES = { ELECTRICAL_CURRENT_AMPERE, "mdi:solar-panel", None, + None, ], "optimizer_power": [ "optimizerpower", @@ -114,6 +124,7 @@ SENSOR_TYPES = { POWER_WATT, "mdi:solar-panel", None, + None, ], "optimizer_temperature": [ "optimizertemperature", @@ -121,6 +132,7 @@ SENSOR_TYPES = { TEMP_CELSIUS, "mdi:solar-panel", None, + DEVICE_CLASS_TEMPERATURE, ], "optimizer_voltage": [ "optimizervoltage", @@ -128,6 +140,7 @@ SENSOR_TYPES = { VOLT, "mdi:solar-panel", None, + None, ], } @@ -170,7 +183,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): TEMP_FAHRENHEIT, "mdi:thermometer", "operating_mode", - None, + DEVICE_CLASS_TEMPERATURE, ] try: @@ -181,6 +194,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): POWER_WATT, "mdi:arrow-collapse-down", None, + None, ] sensors["import_meter_reading"] = [ "totalEnergyimport", @@ -188,6 +202,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ENERGY_WATT_HOUR, "mdi:counter", None, + None, ] except IndexError: _LOGGER.debug("Import meter sensors are not created") @@ -200,6 +215,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): POWER_WATT, "mdi:arrow-expand-up", None, + None, ] sensors["export_meter_reading"] = [ "totalEnergyexport", @@ -207,6 +223,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ENERGY_WATT_HOUR, "mdi:counter", None, + None, ] except IndexError: _LOGGER.debug("Export meter sensors are not created") @@ -225,6 +242,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_info[2], sensor_info[3], sensor_info[4], + sensor_info[5], ) entities.append(sensor) @@ -234,7 +252,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SolarEdgeSensor(SensorEntity): """Representation of an SolarEdge Monitoring API sensor.""" - def __init__(self, platform_name, data, json_key, name, unit, icon, attr): + def __init__( + self, platform_name, data, json_key, name, unit, icon, attr, device_class + ): """Initialize the sensor.""" self._platform_name = platform_name self._data = data @@ -245,6 +265,7 @@ class SolarEdgeSensor(SensorEntity): self._unit_of_measurement = unit self._icon = icon self._attr = attr + self._attr_device_class = device_class @property def name(self): diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index f58c5916bfb..5be89216365 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -11,6 +11,8 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_PROTOCOL, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -18,7 +20,9 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DatatypeDescription = namedtuple("DatatypeDescription", ["name", "unit"]) +DatatypeDescription = namedtuple( + "DatatypeDescription", ["name", "unit", "device_class"] +) CONF_DATATYPE_MASK = "datatype_mask" CONF_ONLY_NAMED = "only_named" @@ -58,20 +62,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_value_descriptions = { tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription( - "temperature", config.get(CONF_TEMPERATURE_SCALE) + "temperature", config.get(CONF_TEMPERATURE_SCALE), DEVICE_CLASS_TEMPERATURE ), tellcore_constants.TELLSTICK_HUMIDITY: DatatypeDescription( - "humidity", PERCENTAGE + "humidity", + PERCENTAGE, + DEVICE_CLASS_HUMIDITY, + ), + tellcore_constants.TELLSTICK_RAINRATE: DatatypeDescription( + "rain rate", "", None + ), + tellcore_constants.TELLSTICK_RAINTOTAL: DatatypeDescription( + "rain total", "", None ), - tellcore_constants.TELLSTICK_RAINRATE: DatatypeDescription("rain rate", ""), - tellcore_constants.TELLSTICK_RAINTOTAL: DatatypeDescription("rain total", ""), tellcore_constants.TELLSTICK_WINDDIRECTION: DatatypeDescription( - "wind direction", "" + "wind direction", "", None ), tellcore_constants.TELLSTICK_WINDAVERAGE: DatatypeDescription( - "wind average", "" + "wind average", "", None + ), + tellcore_constants.TELLSTICK_WINDGUST: DatatypeDescription( + "wind gust", "", None ), - tellcore_constants.TELLSTICK_WINDGUST: DatatypeDescription("wind gust", ""), } try: diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index 18e4b0c7aa1..b1d5ece7d57 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -6,7 +6,7 @@ import voluptuous as vol from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_TYPE, TEMP_CELSIUS +from homeassistant.const import CONF_TYPE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig @@ -46,6 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class XBeeTemperatureSensor(SensorEntity): """Representation of XBee Pro temperature sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_unit_of_measurement = TEMP_CELSIUS def __init__(self, config, device): diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 9731e033972..b35f675440e 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, DEGREE, + DEVICE_CLASS_TEMPERATURE, LENGTH_METERS, PERCENTAGE, PRESSURE_HPA, @@ -43,43 +44,65 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna") SENSOR_TYPES = { - "pressure": ("Pressure", PRESSURE_HPA, "LDstat hPa", float), - "pressure_sealevel": ("Pressure at Sea Level", PRESSURE_HPA, "LDred hPa", float), - "humidity": ("Humidity", PERCENTAGE, "RF %", int), + "pressure": ("Pressure", PRESSURE_HPA, None, "LDstat hPa", float), + "pressure_sealevel": ( + "Pressure at Sea Level", + PRESSURE_HPA, + None, + "LDred hPa", + float, + ), + "humidity": ("Humidity", PERCENTAGE, None, "RF %", int), "wind_speed": ( "Wind Speed", SPEED_KILOMETERS_PER_HOUR, + None, f"WG {SPEED_KILOMETERS_PER_HOUR}", float, ), - "wind_bearing": ("Wind Bearing", DEGREE, f"WR {DEGREE}", int), + "wind_bearing": ("Wind Bearing", DEGREE, None, f"WR {DEGREE}", int), "wind_max_speed": ( "Top Wind Speed", + None, SPEED_KILOMETERS_PER_HOUR, f"WSG {SPEED_KILOMETERS_PER_HOUR}", float, ), - "wind_max_bearing": ("Top Wind Bearing", DEGREE, f"WSR {DEGREE}", int), - "sun_last_hour": ("Sun Last Hour", PERCENTAGE, f"SO {PERCENTAGE}", int), - "temperature": ("Temperature", TEMP_CELSIUS, f"T {TEMP_CELSIUS}", float), + "wind_max_bearing": ("Top Wind Bearing", DEGREE, None, f"WSR {DEGREE}", int), + "sun_last_hour": ("Sun Last Hour", PERCENTAGE, None, f"SO {PERCENTAGE}", int), + "temperature": ( + "Temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + f"T {TEMP_CELSIUS}", + float, + ), "precipitation": ( "Precipitation", + None, f"l/{AREA_SQUARE_METERS}", f"N l/{AREA_SQUARE_METERS}", float, ), - "dewpoint": ("Dew Point", TEMP_CELSIUS, f"TP {TEMP_CELSIUS}", float), + "dewpoint": ( + "Dew Point", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + f"TP {TEMP_CELSIUS}", + float, + ), # The following probably not useful for general consumption, # but we need them to fill in internal attributes - "station_name": ("Station Name", None, "Name", str), + "station_name": ("Station Name", None, None, "Name", str), "station_elevation": ( "Station Elevation", LENGTH_METERS, + None, f"Höhe {LENGTH_METERS}", int, ), - "update_date": ("Update Date", None, "Datum", str), - "update_time": ("Update Time", None, "Zeit", str), + "update_date": ("Update Date", None, None, "Datum", str), + "update_time": ("Update Time", None, None, "Zeit", str), } PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( @@ -140,6 +163,7 @@ class ZamgSensor(SensorEntity): self.probe = probe self.client_name = name self.variable = variable + self._attr_device_class = SENSOR_TYPES[variable][2] @property def name(self): @@ -217,6 +241,7 @@ class ZamgData: api_fields = { col_heading: (standard_name, dtype) for standard_name, ( + _, _, _, col_heading, diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index a3183ba8927..d973e52ff92 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,6 +1,6 @@ """Support for Z-Wave sensors.""" from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN, SensorEntity -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -79,6 +79,13 @@ class ZWaveMultilevelSensor(ZWaveSensor): return self._state + @property + def device_class(self): + """Return the class of this device.""" + if self._units in ["C", "F"]: + return DEVICE_CLASS_TEMPERATURE + return None + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" From 2e44e256f0eb2853d7b6b7f9ed3a257bfeccad76 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 19:17:44 +0200 Subject: [PATCH 222/818] Improve typing of Tasmota (3/3) (#52748) --- homeassistant/components/tasmota/light.py | 88 +++++++++++++--------- homeassistant/components/tasmota/mixins.py | 39 ++++++---- homeassistant/components/tasmota/sensor.py | 50 +++++++----- homeassistant/components/tasmota/switch.py | 31 +++++--- tests/components/tasmota/test_light.py | 24 +++--- 5 files changed, 142 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 675a3d175c3..de25a25fd4f 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -1,4 +1,10 @@ """Support for Tasmota lights.""" +from __future__ import annotations + +from typing import Any + +from hatasmota import light as tasmota_light +from hatasmota.entity import TasmotaEntity as HATasmotaEntity, TasmotaEntityConfig from hatasmota.light import ( LIGHT_TYPE_COLDWARM, LIGHT_TYPE_NONE, @@ -6,6 +12,7 @@ from hatasmota.light import ( LIGHT_TYPE_RGBCW, LIGHT_TYPE_RGBW, ) +from hatasmota.models import DiscoveryHashType from homeassistant.components import light from homeassistant.components.light import ( @@ -25,8 +32,10 @@ from homeassistant.components.light import ( LightEntity, brightness_supported, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW @@ -36,11 +45,17 @@ DEFAULT_BRIGHTNESS_MAX = 255 TASMOTA_BRIGHTNESS_MAX = 100 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Tasmota light dynamically through discovery.""" @callback - def async_discover(tasmota_entity, discovery_hash): + def async_discover( + tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota light.""" async_add_entities( [TasmotaLight(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] @@ -55,12 +70,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def clamp(value): +def clamp(value: float) -> float: """Clamp value to the range 0..255.""" return min(max(value, 0), 255) -def scale_brightness(brightness): +def scale_brightness(brightness: float) -> float: """Scale brightness from 0..255 to 1..100.""" brightness_normalized = brightness / DEFAULT_BRIGHTNESS_MAX device_brightness = min( @@ -79,19 +94,20 @@ class TasmotaLight( ): """Representation of a Tasmota light.""" - def __init__(self, **kwds): + _tasmota_entity: tasmota_light.TasmotaLight + + def __init__(self, **kwds: Any) -> None: """Initialize Tasmota light.""" - self._state = False - self._supported_color_modes = None + self._supported_color_modes: set[str] | None = None self._supported_features = 0 - self._brightness = None - self._color_mode = None - self._color_temp = None - self._effect = None - self._white_value = None + self._brightness: int | None = None + self._color_mode: str | None = None + self._color_temp: int | None = None + self._effect: str | None = None + self._white_value: int | None = None self._flash_times = None - self._hs = None + self._hs: tuple[float, float] | None = None super().__init__( **kwds, @@ -99,13 +115,15 @@ class TasmotaLight( self._setup_from_entity() - async def discovery_update(self, update, write_state=True): + async def discovery_update( + self, update: TasmotaEntityConfig, write_state: bool = True + ) -> None: """Handle updated discovery message.""" await super().discovery_update(update, write_state=False) self._setup_from_entity() self.async_write_ha_state() - def _setup_from_entity(self): + def _setup_from_entity(self) -> None: """(Re)Setup the entity.""" self._supported_color_modes = set() supported_features = 0 @@ -141,7 +159,7 @@ class TasmotaLight( self._supported_features = supported_features @callback - def state_updated(self, state, **kwargs): + def state_updated(self, state: bool, **kwargs: Any) -> None: """Handle state updates.""" self._on_off_state = state attributes = kwargs.get("attributes") @@ -149,7 +167,7 @@ class TasmotaLight( if "brightness" in attributes: brightness = float(attributes["brightness"]) percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX - self._brightness = percent_bright * 255 + self._brightness = round(percent_bright * 255) if "color_hs" in attributes: self._hs = attributes["color_hs"] if "color_temp" in attributes: @@ -159,7 +177,7 @@ class TasmotaLight( if "white_value" in attributes: white_value = float(attributes["white_value"]) percent_white = white_value / TASMOTA_BRIGHTNESS_MAX - self._white_value = percent_white * 255 + self._white_value = round(percent_white * 255) if self._tasmota_entity.light_type == LIGHT_TYPE_RGBW: # Tasmota does not support RGBW mode, set mode to white or hs if self._white_value == 0: @@ -176,68 +194,68 @@ class TasmotaLight( self.async_write_ha_state() @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return self._brightness @property - def color_mode(self): + def color_mode(self) -> str | None: """Return the color mode of the light.""" return self._color_mode @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature in mired.""" return self._color_temp @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" return self._tasmota_entity.min_mireds @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" return self._tasmota_entity.max_mireds @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" return self._effect @property - def effect_list(self): + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return self._tasmota_entity.effect_list @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hs color value.""" if self._hs is None: return None hs_color = self._hs - return [hs_color[0], hs_color[1]] + return (hs_color[0], hs_color[1]) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return False @property - def supported_color_modes(self): + def supported_color_modes(self) -> set[str] | None: """Flag supported color modes.""" return self._supported_color_modes @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - supported_color_modes = self._supported_color_modes + supported_color_modes = self._supported_color_modes or set() - attributes = {} + attributes: dict[str, Any] = {} if ATTR_HS_COLOR in kwargs and COLOR_MODE_HS in supported_color_modes: hs_color = kwargs[ATTR_HS_COLOR] @@ -260,7 +278,7 @@ class TasmotaLight( self._tasmota_entity.set_state(True, attributes) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" attributes = {"state": "OFF"} diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index f1b0554957e..a07e48b53a7 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -4,6 +4,13 @@ from __future__ import annotations import logging from typing import Any +from hatasmota.entity import ( + TasmotaAvailability as HATasmotaAvailability, + TasmotaEntity as HATasmotaEntity, + TasmotaEntityConfig, +) +from hatasmota.models import DiscoveryHashType + from homeassistant.components.mqtt import ( async_subscribe_connection_status, is_connected as mqtt_connected, @@ -11,7 +18,7 @@ from homeassistant.components.mqtt import ( from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .discovery import ( TASMOTA_DISCOVERY_ENTITY_UPDATED, @@ -25,48 +32,50 @@ _LOGGER = logging.getLogger(__name__) class TasmotaEntity(Entity): """Base class for Tasmota entities.""" - def __init__(self, tasmota_entity) -> None: + def __init__(self, tasmota_entity: HATasmotaEntity) -> None: """Initialize.""" self._tasmota_entity = tasmota_entity self._unique_id = tasmota_entity.unique_id - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" await self._subscribe_topics() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" await self._tasmota_entity.unsubscribe_topics() await super().async_will_remove_from_hass() - async def discovery_update(self, update, write_state=True): + async def discovery_update( + self, update: TasmotaEntityConfig, write_state: bool = True + ) -> None: """Handle updated discovery message.""" self._tasmota_entity.config_update(update) await self._subscribe_topics() if write_state: self.async_write_ha_state() - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await self._tasmota_entity.subscribe_topics() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return {"connections": {(CONNECTION_NETWORK_MAC, self._tasmota_entity.mac)}} @property - def name(self): + def name(self) -> str | None: """Return the name of the binary sensor.""" return self._tasmota_entity.name @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling state.""" return False @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return self._unique_id @@ -99,7 +108,9 @@ class TasmotaOnOffEntity(TasmotaEntity): class TasmotaAvailability(TasmotaEntity): """Mixin used for platforms that report availability.""" - def __init__(self, **kwds) -> None: + _tasmota_entity: HATasmotaAvailability + + def __init__(self, **kwds: Any) -> None: """Initialize the availability mixin.""" self._available = False super().__init__(**kwds) @@ -120,7 +131,7 @@ class TasmotaAvailability(TasmotaEntity): self.async_write_ha_state() @callback - def async_mqtt_connected(self, _): + def async_mqtt_connected(self, _: bool) -> None: """Update state on connection/disconnection to MQTT broker.""" if not self.hass.is_stopping: if not mqtt_connected(self.hass): @@ -136,7 +147,7 @@ class TasmotaAvailability(TasmotaEntity): class TasmotaDiscoveryUpdate(TasmotaEntity): """Mixin used to handle updated discovery message.""" - def __init__(self, discovery_hash, **kwds) -> None: + def __init__(self, discovery_hash: DiscoveryHashType, **kwds: Any) -> None: """Initialize the discovery update mixin.""" self._discovery_hash = discovery_hash self._removed_from_hass = False @@ -147,7 +158,7 @@ class TasmotaDiscoveryUpdate(TasmotaEntity): self._removed_from_hass = False await super().async_added_to_hass() - async def discovery_callback(config): + async def discovery_callback(config: TasmotaEntityConfig) -> None: """Handle discovery update.""" _LOGGER.debug( "Got update for entity with hash: %s '%s'", diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index fa4a8270cc7..87b81322799 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -1,12 +1,17 @@ """Support for Tasmota sensors.""" from __future__ import annotations +from datetime import datetime import logging +from typing import Any from hatasmota import const as hc, sensor as tasmota_sensor, status_sensor +from hatasmota.entity import TasmotaEntity as HATasmotaEntity +from hatasmota.models import DiscoveryHashType from homeassistant.components import sensor from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -41,8 +46,9 @@ from homeassistant.const import ( TEMP_KELVIN, VOLT, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import DATA_REMOVE_DISCOVER_COMPONENT @@ -150,10 +156,17 @@ SENSOR_UNIT_MAP = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Tasmota sensor dynamically through discovery.""" - async def async_discover_sensor(tasmota_entity, discovery_hash): + @callback + def async_discover( + tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota sensor.""" async_add_entities( [ @@ -168,7 +181,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] = async_dispatcher_connect( hass, TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN), - async_discover_sensor, + async_discover, ) @@ -178,9 +191,10 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): _attr_last_reset = None _tasmota_entity: tasmota_sensor.TasmotaSensor - def __init__(self, **kwds): + def __init__(self, **kwds: Any) -> None: """Initialize the Tasmota sensor.""" - self._state = None + self._state: Any | None = None + self._state_timestamp: datetime | None = None super().__init__( **kwds, @@ -192,14 +206,16 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): await super().async_added_to_hass() @callback - def sensor_state_updated(self, state, **kwargs): + def sensor_state_updated(self, state: Any, **kwargs: Any) -> None: """Handle state updates.""" - self._state = state + if self.device_class == DEVICE_CLASS_TIMESTAMP: + self._state_timestamp = state + else: + self._state = state if "last_reset" in kwargs: try: - last_reset = dt_util.as_utc( - dt_util.parse_datetime(kwargs["last_reset"]) - ) + last_reset_dt = dt_util.parse_datetime(kwargs["last_reset"]) + last_reset = dt_util.as_utc(last_reset_dt) if last_reset_dt else None if last_reset is None: raise ValueError self._attr_last_reset = last_reset @@ -234,7 +250,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return True @property - def icon(self): + def icon(self) -> str | None: """Return the icon.""" class_or_icon = SENSOR_DEVICE_CLASS_ICON_MAP.get( self._tasmota_entity.quantity, {} @@ -242,18 +258,18 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return class_or_icon.get(ICON) @property - def state(self): + def state(self) -> str | None: """Return the state of the entity.""" - if self._state and self.device_class == DEVICE_CLASS_TIMESTAMP: - return self._state.isoformat() + if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: + return self._state_timestamp.isoformat() return self._state @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return True @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return SENSOR_UNIT_MAP.get(self._tasmota_entity.unit, self._tasmota_entity.unit) diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py index f7fa67bed22..50319abac56 100644 --- a/homeassistant/components/tasmota/switch.py +++ b/homeassistant/components/tasmota/switch.py @@ -1,20 +1,33 @@ """Support for Tasmota switches.""" +from typing import Any + +from hatasmota import relay as tasmota_relay +from hatasmota.entity import TasmotaEntity as HATasmotaEntity +from hatasmota.models import DiscoveryHashType from homeassistant.components import switch from homeassistant.components.switch import SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate, TasmotaOnOffEntity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Tasmota switch dynamically through discovery.""" @callback - def async_discover(tasmota_entity, discovery_hash): + def async_discover( + tasmota_entity: HATasmotaEntity, discovery_hash: DiscoveryHashType + ) -> None: """Discover and add a Tasmota switch.""" async_add_entities( [ @@ -41,18 +54,12 @@ class TasmotaSwitch( ): """Representation of a Tasmota switch.""" - def __init__(self, **kwds): - """Initialize the Tasmota switch.""" - self._state = False + _tasmota_entity: tasmota_relay.TasmotaRelay - super().__init__( - **kwds, - ) - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._tasmota_entity.set_state(True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._tasmota_entity.set_state(False) diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index e1ba2615742..411567208db 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -384,7 +384,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( @@ -402,7 +402,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -446,7 +446,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( @@ -454,7 +454,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 191.25 + assert state.attributes.get("brightness") == 191 assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( @@ -464,7 +464,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -473,7 +473,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") is None assert state.attributes.get("color_mode") == "white" @@ -544,7 +544,7 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( @@ -645,7 +645,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( @@ -1216,7 +1216,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 # Dim the light from 50->0: Speed should be 6*2*2=24 await common.async_turn_off(hass, "light.test", transition=6) @@ -1254,7 +1254,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") == (0, 255, 0) # Set color of the light from 0,255,0 to 255,0,0 @ 50%: Speed should be 6*2*2=24 @@ -1296,7 +1296,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_temp") == 153 # Set color_temp of the light from 153 to 500 @ 50%: Speed should be 6*2*2=24 @@ -1315,7 +1315,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_temp") == 500 # Set color_temp of the light from 500 to 326 @ 50%: Speed should be 6*2*2*2=48->40 From ec8f11f1e3ad77dc9547a7214e324a63a1d93a0a Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 12 Jul 2021 14:02:56 -0400 Subject: [PATCH 223/818] Bump pyinsteon to 1.0.11 (#52927) --- homeassistant/components/insteon/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index dc564ae0d70..353cd55c747 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,8 +2,12 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.9"], - "codeowners": ["@teharris1"], + "requirements": [ + "pyinsteon==1.0.11" + ], + "codeowners": [ + "@teharris1" + ], "config_flow": true, "iot_class": "local_push" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 2c5f5995925..77c7b3868d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1488,7 +1488,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.9 +pyinsteon==1.0.11 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cf667419a2..a559a7a7e85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -835,7 +835,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.9 +pyinsteon==1.0.11 # homeassistant.components.ipma pyipma==2.0.5 From 5c200581b6c730857b5857edf6a7014ae612cfdb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 12 Jul 2021 20:03:40 +0200 Subject: [PATCH 224/818] Upgrade sentry-sdk to 1.3.0 (#52926) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 0b37e6a849a..d6ddf61f19a 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.1.0"], + "requirements": ["sentry-sdk==1.3.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 77c7b3868d4..0a4ebf44e55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2082,7 +2082,7 @@ sense-hat==2.2.0 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==1.1.0 +sentry-sdk==1.3.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a559a7a7e85..e4f59f1656f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1137,7 +1137,7 @@ screenlogicpy==0.4.1 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==1.1.0 +sentry-sdk==1.3.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 5caf170c78c5da0d7592a489d9da46d3b855dd99 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 12 Jul 2021 20:06:31 +0200 Subject: [PATCH 225/818] Correct Wrong "raise" in modbus validators. (#52924) --- homeassistant/components/modbus/validators.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 6e8b740c415..e2777547ce4 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -78,10 +78,8 @@ def sensor_schema_validator(config): if config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: try: structure = f">{DEFAULT_STRUCT_FORMAT[data_type]}" - except KeyError: - raise vol.Invalid( - f"Modbus error {data_type} unknown in {name}" - ) from KeyError + except KeyError as exp: + raise vol.Invalid(f"Modbus error {data_type} unknown in {name}") from exp else: if not structure: raise vol.Invalid( From 5e472f2c0647cfa115ca4bc520fae8570fb18734 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 20:14:03 +0200 Subject: [PATCH 226/818] Improve typing of Tasmota (1/3) (#52746) --- homeassistant/components/mqtt/discovery.py | 10 ++- homeassistant/components/tasmota/__init__.py | 77 +++++++++++++------ .../components/tasmota/config_flow.py | 26 +++++-- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index e0d1d0eb4dd..84d85fba79c 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -275,8 +275,16 @@ async def async_start( # noqa: C901 if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: return + data = { + "topic": msg.topic, + "payload": msg.payload, + "qos": msg.qos, + "retain": msg.retain, + "subscribed_topic": msg.subscribed_topic, + "timestamp": msg.timestamp, + } result = await hass.config_entries.flow.async_init( - integration, context={"source": DOMAIN}, data=msg + integration, context={"source": DOMAIN}, data=data ) if ( result diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index af7f9222c50..27cfed82652 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -1,4 +1,6 @@ """The Tasmota integration.""" +from __future__ import annotations + import asyncio import logging @@ -10,6 +12,7 @@ from hatasmota.const import ( CONF_SW_VERSION, ) from hatasmota.discovery import clear_discovery_topic +from hatasmota.models import TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient import voluptuous as vol @@ -18,10 +21,13 @@ from homeassistant.components.mqtt.subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) -from homeassistant.core import callback +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, EVENT_DEVICE_REGISTRY_UPDATED, + DeviceRegistry, async_entries_for_config_entry, ) @@ -37,33 +43,38 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tasmota from a config entry.""" websocket_api.async_register_command(hass, websocket_remove_device) hass.data[DATA_UNSUB] = [] - def _publish(*args, **kwds): - mqtt.async_publish(hass, *args, **kwds) + def _publish( + topic: str, + payload: mqtt.PublishPayloadType, + qos: int | None = None, + retain: bool | None = None, + ) -> None: + mqtt.async_publish(hass, topic, payload, qos, retain) - async def _subscribe_topics(sub_state, topics): + async def _subscribe_topics(sub_state: dict | None, topics: dict) -> dict: # Optionally mark message handlers as callback for topic in topics.values(): if "msg_callback" in topic and "event_loop_safe" in topic: topic["msg_callback"] = callback(topic["msg_callback"]) return await async_subscribe_topics(hass, sub_state, topics) - async def _unsubscribe_topics(sub_state): + async def _unsubscribe_topics(sub_state: dict | None) -> dict: return await async_unsubscribe_topics(hass, sub_state) tasmota_mqtt = TasmotaMQTTClient(_publish, _subscribe_topics, _unsubscribe_topics) device_registry = await hass.helpers.device_registry.async_get_registry() - def async_discover_device(config, mac): + def async_discover_device(config: TasmotaDeviceConfig, mac: str) -> None: """Discover and add a Tasmota device.""" async_setup_device(hass, mac, config, entry, tasmota_mqtt, device_registry) - async def async_device_removed(event): + async def async_device_removed(event: Event) -> None: """Handle the removal of a device.""" device_registry = await hass.helpers.device_registry.async_get_registry() if event.data["action"] != "remove": @@ -82,7 +93,7 @@ async def async_setup_entry(hass, entry): hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed) ) - async def start_platforms(): + async def start_platforms() -> None: await device_automation.async_setup_entry(hass, entry) await asyncio.gather( *[ @@ -100,7 +111,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # cleanup platforms @@ -127,7 +138,13 @@ async def async_unload_entry(hass, entry): return True -def _remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry): +def _remove_device( + hass: HomeAssistant, + config_entry: ConfigEntry, + mac: str, + tasmota_mqtt: TasmotaMQTTClient, + device_registry: DeviceRegistry, +) -> None: """Remove device from device registry.""" device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) @@ -139,22 +156,32 @@ def _remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry): clear_discovery_topic(mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt) -def _update_device(hass, config_entry, config, device_registry): +def _update_device( + hass: HomeAssistant, + config_entry: ConfigEntry, + config: TasmotaDeviceConfig, + device_registry: DeviceRegistry, +) -> None: """Add or update device registry.""" - config_entry_id = config_entry.entry_id - device_info = { - "connections": {(CONNECTION_NETWORK_MAC, config[CONF_MAC])}, - "manufacturer": config[CONF_MANUFACTURER], - "model": config[CONF_MODEL], - "name": config[CONF_NAME], - "sw_version": config[CONF_SW_VERSION], - "config_entry_id": config_entry_id, - } _LOGGER.debug("Adding or updating tasmota device %s", config[CONF_MAC]) - device_registry.async_get_or_create(**device_info) + device_registry.async_get_or_create( + connections={(CONNECTION_NETWORK_MAC, config[CONF_MAC])}, + manufacturer=config[CONF_MANUFACTURER], + model=config[CONF_MODEL], + name=config[CONF_NAME], + sw_version=config[CONF_SW_VERSION], + config_entry_id=config_entry.entry_id, + ) -def async_setup_device(hass, mac, config, config_entry, tasmota_mqtt, device_registry): +def async_setup_device( + hass: HomeAssistant, + mac: str, + config: TasmotaDeviceConfig, + config_entry: ConfigEntry, + tasmota_mqtt: TasmotaMQTTClient, + device_registry: DeviceRegistry, +) -> None: """Set up the Tasmota device.""" if not config: _remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry) @@ -166,7 +193,9 @@ def async_setup_device(hass, mac, config, config_entry, tasmota_mqtt, device_reg {vol.Required("type"): "tasmota/device/remove", vol.Required("device_id"): str} ) @websocket_api.async_response -async def websocket_remove_device(hass, connection, msg): +async def websocket_remove_device( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Delete device.""" device_id = msg["device_id"] dev_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index 2f15b73c6e9..e1621f2c126 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -1,8 +1,14 @@ """Config flow for Tasmota.""" +from __future__ import annotations + +from typing import Any, cast + import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import valid_subscribe_topic +from homeassistant.components.mqtt import ReceiveMessage, valid_subscribe_topic +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN @@ -12,11 +18,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize flow.""" self._prefix = DEFAULT_PREFIX - async def async_step_mqtt(self, discovery_info=None): + async def async_step_mqtt(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by MQTT discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -24,7 +30,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(DOMAIN) # Validate the topic, will throw if it fails - prefix = discovery_info.subscribed_topic + prefix = cast(ReceiveMessage, discovery_info).subscribed_topic if prefix.endswith("/#"): prefix = prefix[:-2] try: @@ -36,7 +42,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -45,7 +53,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_config() return await self.async_step_confirm() - async def async_step_config(self, user_input=None): + async def async_step_config( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm the setup.""" errors = {} data = {CONF_DISCOVERY_PREFIX: self._prefix} @@ -72,7 +82,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="config", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_confirm(self, user_input=None): + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm the setup.""" data = {CONF_DISCOVERY_PREFIX: self._prefix} From 8f8935c85938962c4b61c23eb4e5bb4f0dc6327b Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 12 Jul 2021 20:21:10 +0200 Subject: [PATCH 227/818] Bump python-fireservicerota to 0.0.42 (#52807) --- homeassistant/components/fireservicerota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json index 0e2259b6b5e..f35be9e839f 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -3,7 +3,7 @@ "name": "FireServiceRota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fireservicerota", - "requirements": ["pyfireservicerota==0.0.40"], + "requirements": ["pyfireservicerota==0.0.42"], "codeowners": ["@cyberjunky"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0a4ebf44e55..08baeb59c3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1421,7 +1421,7 @@ pyezviz==0.1.8.9 pyfido==2.1.1 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.40 +pyfireservicerota==0.0.42 # homeassistant.components.flexit pyflexit==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4f59f1656f..e5aeac43f33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -789,7 +789,7 @@ pyezviz==0.1.8.9 pyfido==2.1.1 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.40 +pyfireservicerota==0.0.42 # homeassistant.components.flume pyflume==0.5.5 From 646862ec96f6b3a2eef1536aad9b49769f49e9d4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 12 Jul 2021 20:22:53 +0200 Subject: [PATCH 228/818] Add array write to turn_on/off in modbus switch/fan/light (#52582) --- homeassistant/components/modbus/__init__.py | 4 ++++ .../components/modbus/base_platform.py | 24 +++++++++++++++---- homeassistant/components/modbus/const.py | 2 ++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8ccfe45f86c..9f851b4a235 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -54,6 +54,8 @@ from .const import ( CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_X_COILS, + CALL_TYPE_X_REGISTER_HOLDINGS, CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLIMATES, @@ -185,6 +187,8 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( [ CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL, + CALL_TYPE_X_COILS, + CALL_TYPE_X_REGISTER_HOLDINGS, ] ), vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index ed2f6e69863..9c21ba3970c 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -23,8 +23,13 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( CALL_TYPE_COIL, + CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_COILS, CALL_TYPE_WRITE_REGISTER, + CALL_TYPE_WRITE_REGISTERS, + CALL_TYPE_X_COILS, + CALL_TYPE_X_REGISTER_HOLDINGS, CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, @@ -92,10 +97,19 @@ class BaseSwitch(BasePlatform, RestoreEntity): config[CONF_INPUT_TYPE] = "" super().__init__(hub, config) self._is_on = None - if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: - self._write_type = CALL_TYPE_WRITE_COIL - else: - self._write_type = CALL_TYPE_WRITE_REGISTER + convert = { + CALL_TYPE_REGISTER_HOLDING: ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_WRITE_REGISTER, + ), + CALL_TYPE_COIL: (CALL_TYPE_COIL, CALL_TYPE_WRITE_COIL), + CALL_TYPE_X_COILS: (CALL_TYPE_COIL, CALL_TYPE_WRITE_COILS), + CALL_TYPE_X_REGISTER_HOLDINGS: ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_WRITE_REGISTERS, + ), + } + self._write_type = convert[config[CONF_WRITE_TYPE]][1] self.command_on = config[CONF_COMMAND_ON] self._command_off = config[CONF_COMMAND_OFF] if CONF_VERIFY in config: @@ -107,7 +121,7 @@ class BaseSwitch(BasePlatform, RestoreEntity): CONF_ADDRESS, config[CONF_ADDRESS] ) self._verify_type = config[CONF_VERIFY].get( - CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] + CONF_INPUT_TYPE, convert[config[CONF_WRITE_TYPE]][0] ) self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self.command_on) self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 2607fd2bdf0..62319bcde85 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -98,6 +98,8 @@ CALL_TYPE_WRITE_COIL = "write_coil" CALL_TYPE_WRITE_COILS = "write_coils" CALL_TYPE_WRITE_REGISTER = "write_register" CALL_TYPE_WRITE_REGISTERS = "write_registers" +CALL_TYPE_X_COILS = "coils" +CALL_TYPE_X_REGISTER_HOLDINGS = "holdings" # service calls SERVICE_WRITE_COIL = "write_coil" From 3e09787d850e22b0b9ea2ca19ed67be0a796e3ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 20:32:55 +0200 Subject: [PATCH 229/818] Set device_class on temperature sensors F-K (#52918) * Set device_class on temperature sensors F-K * Fix juicenet sensor --- homeassistant/components/foobot/sensor.py | 24 +++++-- homeassistant/components/fritzbox/sensor.py | 3 +- homeassistant/components/glances/const.py | 69 ++++++++++++++------- homeassistant/components/glances/sensor.py | 5 ++ homeassistant/components/hddtemp/sensor.py | 6 ++ homeassistant/components/ihc/sensor.py | 12 +++- homeassistant/components/juicenet/sensor.py | 32 +++++++--- homeassistant/components/kaiterra/sensor.py | 13 +++- 8 files changed, 123 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 996ac1b1049..d635f231818 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_TOKEN, CONF_USERNAME, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, TIME_SECONDS, @@ -36,17 +37,23 @@ ATTR_VOLATILE_ORGANIC_COMPOUNDS = "VOC" ATTR_FOOBOT_INDEX = "index" SENSOR_TYPES = { - "time": [ATTR_TIME, TIME_SECONDS], - "pm": [ATTR_PM2_5, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "mdi:cloud"], - "tmp": [ATTR_TEMPERATURE, TEMP_CELSIUS, "mdi:thermometer"], - "hum": [ATTR_HUMIDITY, PERCENTAGE, "mdi:water-percent"], - "co2": [ATTR_CARBON_DIOXIDE, CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2"], + "time": [ATTR_TIME, TIME_SECONDS, None, None], + "pm": [ATTR_PM2_5, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "mdi:cloud", None], + "tmp": [ATTR_TEMPERATURE, TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + "hum": [ATTR_HUMIDITY, PERCENTAGE, "mdi:water-percent", None], + "co2": [ + ATTR_CARBON_DIOXIDE, + CONCENTRATION_PARTS_PER_MILLION, + "mdi:molecule-co2", + None, + ], "voc": [ ATTR_VOLATILE_ORGANIC_COMPOUNDS, CONCENTRATION_PARTS_PER_BILLION, "mdi:cloud", + None, ], - "allpollu": [ATTR_FOOBOT_INDEX, PERCENTAGE, "mdi:percent"], + "allpollu": [ATTR_FOOBOT_INDEX, PERCENTAGE, "mdi:percent", None], } SCAN_INTERVAL = timedelta(minutes=10) @@ -108,6 +115,11 @@ class FoobotSensor(SensorEntity): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES[self.type][3] + @property def icon(self): """Icon to use in the frontend.""" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index ceae0bb757f..db50776d69c 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -44,7 +45,7 @@ async def async_setup_entry( ATTR_NAME: f"{device.name}", ATTR_ENTITY_ID: f"{device.ain}", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, }, coordinator, ain, diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 18865a232d7..8e20fbfa46b 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,7 +1,13 @@ """Constants for Glances component.""" import sys -from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + DATA_GIBIBYTES, + DATA_MEBIBYTES, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) DOMAIN = "glances" CONF_VERSION = "version" @@ -21,31 +27,50 @@ else: CPU_ICON = "mdi:cpu-32-bit" SENSOR_TYPES = { - "disk_use_percent": ["fs", "used percent", PERCENTAGE, "mdi:harddisk"], - "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk"], - "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk"], - "memory_use_percent": ["mem", "RAM used percent", PERCENTAGE, "mdi:memory"], - "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory"], - "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory"], - "swap_use_percent": ["memswap", "Swap used percent", PERCENTAGE, "mdi:memory"], - "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory"], - "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory"], - "processor_load": ["load", "CPU load", "15 min", CPU_ICON], - "process_running": ["processcount", "Running", "Count", CPU_ICON], - "process_total": ["processcount", "Total", "Count", CPU_ICON], - "process_thread": ["processcount", "Thread", "Count", CPU_ICON], - "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON], - "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON], - "temperature_core": ["sensors", "Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "temperature_hdd": ["sensors", "Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "fan_speed": ["sensors", "Fan speed", "RPM", "mdi:fan"], - "battery": ["sensors", "Charge", PERCENTAGE, "mdi:battery"], - "docker_active": ["docker", "Containers active", "", "mdi:docker"], - "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker"], + "disk_use_percent": ["fs", "used percent", PERCENTAGE, "mdi:harddisk", None], + "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk", None], + "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk", None], + "memory_use_percent": ["mem", "RAM used percent", PERCENTAGE, "mdi:memory", None], + "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory", None], + "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory", None], + "swap_use_percent": [ + "memswap", + "Swap used percent", + PERCENTAGE, + "mdi:memory", + None, + ], + "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory", None], + "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory", None], + "processor_load": ["load", "CPU load", "15 min", CPU_ICON, None], + "process_running": ["processcount", "Running", "Count", CPU_ICON, None], + "process_total": ["processcount", "Total", "Count", CPU_ICON, None], + "process_thread": ["processcount", "Thread", "Count", CPU_ICON, None], + "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON, None], + "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON, None], + "temperature_core": [ + "sensors", + "Temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "temperature_hdd": [ + "sensors", + "Temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "fan_speed": ["sensors", "Fan speed", "RPM", "mdi:fan", None], + "battery": ["sensors", "Charge", PERCENTAGE, "mdi:battery", None], + "docker_active": ["docker", "Containers active", "", "mdi:docker", None], + "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker", None], "docker_memory_use": [ "docker", "Containers RAM used", DATA_MEBIBYTES, "mdi:docker", + None, ], } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7e599af414c..8306543f700 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -91,6 +91,11 @@ class GlancesSensor(SensorEntity): """Set unique_id for sensor.""" return f"{self.glances_data.host}-{self.name}" + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self.sensor_details[4] + @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 4376c7f1289..8169fa811e0 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -81,6 +82,11 @@ class HddTempSensor(SensorEntity): """Return the state of the device.""" return self._state + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_TEMPERATURE + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index 3348e857f51..d1aec781df7 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,6 +1,7 @@ """Support for IHC sensors.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_TEMPERATURE +from homeassistant.util.unit_system import TEMPERATURE_UNITS from . import IHC_CONTROLLER, IHC_INFO from .ihcdevice import IHCDevice @@ -37,6 +38,15 @@ class IHCSensor(IHCDevice, SensorEntity): self._state = None self._unit_of_measurement = unit + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return ( + DEVICE_CLASS_TEMPERATURE + if self._unit_of_measurement in TEMPERATURE_UNITS + else None + ) + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 7564c6e4344..81fabf17eea 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,6 +1,11 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, ELECTRICAL_CURRENT_AMPERE, ENERGY_WATT_HOUR, POWER_WATT, @@ -13,13 +18,23 @@ from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR from .entity import JuiceNetDevice SENSOR_TYPES = { - "status": ["Charging Status", None, None], - "temperature": ["Temperature", TEMP_CELSIUS, STATE_CLASS_MEASUREMENT], - "voltage": ["Voltage", VOLT, None], - "amps": ["Amps", ELECTRICAL_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT], - "watts": ["Watts", POWER_WATT, STATE_CLASS_MEASUREMENT], - "charge_time": ["Charge time", TIME_SECONDS, None], - "energy_added": ["Energy added", ENERGY_WATT_HOUR, None], + "status": ["Charging Status", None, None, None], + "temperature": [ + "Temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ], + "voltage": ["Voltage", VOLT, DEVICE_CLASS_VOLTAGE, None], + "amps": [ + "Amps", + ELECTRICAL_CURRENT_AMPERE, + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + ], + "watts": ["Watts", POWER_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT], + "charge_time": ["Charge time", TIME_SECONDS, None, None], + "energy_added": ["Energy added", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, None], } @@ -44,7 +59,8 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): super().__init__(device, sensor_type, coordinator) self._name = SENSOR_TYPES[sensor_type][0] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._attr_state_class = SENSOR_TYPES[sensor_type][2] + self._attr_device_class = SENSOR_TYPES[sensor_type][2] + self._attr_state_class = SENSOR_TYPES[sensor_type][3] @property def name(self): diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 1e4dd0cbbca..6c82013361a 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,13 +1,20 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_KAITERRA, DOMAIN SENSORS = [ - {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, - {"name": "Humidity", "prop": "rhumid", "device_class": "humidity"}, + {"name": "Temperature", "prop": "rtemp", "device_class": DEVICE_CLASS_TEMPERATURE}, + {"name": "Humidity", "prop": "rhumid", "device_class": DEVICE_CLASS_HUMIDITY}, ] From 0a3aab935a594824c1dd5755e28b870572206eab Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 12 Jul 2021 20:40:16 +0200 Subject: [PATCH 230/818] Use properties instead of raw data in the rituals integration (#52587) --- .../rituals_perfume_genie/__init__.py | 9 ++++--- .../rituals_perfume_genie/config_flow.py | 6 ++--- .../components/rituals_perfume_genie/const.py | 6 ++--- .../rituals_perfume_genie/entity.py | 17 ++++--------- .../rituals_perfume_genie/manifest.json | 2 +- .../rituals_perfume_genie/sensor.py | 24 ++++++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../rituals_perfume_genie/test_config_flow.py | 3 ++- 9 files changed, 26 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 84fc5ed2cf5..26dbfca08d9 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT +from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN PLATFORMS = ["binary_sensor", "number", "select", "sensor", "switch"] @@ -23,8 +23,7 @@ UPDATE_INTERVAL = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Rituals Perfume Genie from a config entry.""" session = async_get_clientsession(hass) - account = Account(session=session) - account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} + account = Account(session=session, account_hash=entry.data[ACCOUNT_HASH]) try: account_devices = await account.get_devices() @@ -37,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } for device in account_devices: - hublot = device.hub_data[HUBLOT] + hublot = device.hublot coordinator = RitualsDataUpdateCoordinator(hass, device) await coordinator.async_refresh() @@ -68,7 +67,7 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator): super().__init__( hass, _LOGGER, - name=f"{DOMAIN}-{device.hub_data[HUBLOT]}", + name=f"{DOMAIN}-{device.hublot}", update_interval=UPDATE_INTERVAL, ) diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index f1f037941b3..294dced4217 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -47,12 +47,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(account.data[CONF_EMAIL]) + await self.async_set_unique_id(account.email) self._abort_if_unique_id_configured() return self.async_create_entry( - title=account.data[CONF_EMAIL], - data={ACCOUNT_HASH: account.data[ACCOUNT_HASH]}, + title=account.email, + data={ACCOUNT_HASH: account.account_hash}, ) return self.async_show_form( diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index bafdef9140c..21c570ffb93 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -1,9 +1,7 @@ """Constants for the Rituals Perfume Genie integration.""" DOMAIN = "rituals_perfume_genie" +ACCOUNT_HASH = "account_hash" + COORDINATORS = "coordinators" DEVICES = "devices" - -ACCOUNT_HASH = "account_hash" -HUBLOT = "hublot" -SENSORS = "sensors" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 19c3f3cd424..16c4e76686c 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -6,19 +6,12 @@ from pyrituals import Diffuser from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RitualsDataUpdateCoordinator -from .const import DOMAIN, HUBLOT, SENSORS +from .const import DOMAIN MANUFACTURER = "Rituals Cosmetics" MODEL = "The Perfume Genie" MODEL2 = "The Perfume Genie 2.0" -ATTRIBUTES = "attributes" -ROOMNAME = "roomnamec" -STATUS = "status" -VERSION = "versionc" - -AVAILABLE_STATE = 1 - class DiffuserEntity(CoordinatorEntity): """Representation of a diffuser entity.""" @@ -35,8 +28,8 @@ class DiffuserEntity(CoordinatorEntity): super().__init__(coordinator) self._diffuser = diffuser - hublot = self._diffuser.hub_data[HUBLOT] - hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME] + hublot = self._diffuser.hublot + hubname = self._diffuser.name self._attr_name = f"{hubname}{entity_suffix}" self._attr_unique_id = f"{hublot}{entity_suffix}" @@ -45,10 +38,10 @@ class DiffuserEntity(CoordinatorEntity): "identifiers": {(DOMAIN, hublot)}, "manufacturer": MANUFACTURER, "model": MODEL if diffuser.has_battery else MODEL2, - "sw_version": diffuser.hub_data[SENSORS][VERSION], + "sw_version": diffuser.version, } @property def available(self) -> bool: """Return if the entity is available.""" - return super().available and self._diffuser.hub_data[STATUS] == AVAILABLE_STATE + return super().available and self._diffuser.is_online diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 2736b960751..6c46649f688 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,7 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": ["pyrituals==0.0.4"], + "requirements": ["pyrituals==0.0.5"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 2965371733b..d4e10ba141b 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -13,16 +13,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import COORDINATORS, DEVICES, DOMAIN, SENSORS +from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity -ID = "id" -PERFUME = "rfidc" -FILL = "fillc" - -PERFUME_NO_CARTRIDGE_ID = 19 -FILL_NO_CARTRIDGE_ID = 12 - BATTERY_SUFFIX = " Battery" PERFUME_SUFFIX = " Perfume" FILL_SUFFIX = " Fill" @@ -58,9 +51,9 @@ class DiffuserPerfumeSensor(DiffuserEntity): """Initialize the perfume sensor.""" super().__init__(diffuser, coordinator, PERFUME_SUFFIX) - self._attr_icon = "mdi:tag-text" - if diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: - self._attr_icon = "mdi:tag-remove" + self._attr_icon = "mdi:tag-remove" + if diffuser.has_cartridge: + self._attr_icon = "mdi:tag-text" @property def state(self) -> str: @@ -77,12 +70,9 @@ class DiffuserFillSensor(DiffuserEntity): """Initialize the fill sensor.""" super().__init__(diffuser, coordinator, FILL_SUFFIX) - @property - def icon(self) -> str: - """Return the fill sensor icon.""" - if self._diffuser.hub_data[SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: - return "mdi:beaker-question" - return "mdi:beaker" + self._attr_icon = "mdi:beaker-question" + if diffuser.has_cartridge: + self.attr_icon = "mdi:beaker" @property def state(self) -> str: diff --git a/requirements_all.txt b/requirements_all.txt index 08baeb59c3f..2149b72b22e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1706,7 +1706,7 @@ pyrepetier==3.0.5 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.4 +pyrituals==0.0.5 # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5aeac43f33..c5afe5edbcf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -969,7 +969,7 @@ pyqwikswitch==0.93 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.4 +pyrituals==0.0.5 # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index e5c64dd54c9..df40405a56b 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -16,7 +16,8 @@ WRONG_PASSWORD = "wrong-passw0rd" def _mock_account(*_): account = MagicMock() account.authenticate = AsyncMock() - account.data = {CONF_EMAIL: TEST_EMAIL, ACCOUNT_HASH: "any"} + account.account_hash = "any" + account.email = TEST_EMAIL return account From d1f3c200795838a12581831f4f38f90b6444fd4a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 20:41:45 +0200 Subject: [PATCH 231/818] Set device_class on temperature sensors L-Q (#52919) --- homeassistant/components/lacrosse/sensor.py | 2 + .../components/luftdaten/__init__.py | 33 ++++++++- homeassistant/components/luftdaten/sensor.py | 13 +++- homeassistant/components/mfi/sensor.py | 16 +++- homeassistant/components/mysensors/sensor.py | 74 +++++++++++-------- homeassistant/components/notion/sensor.py | 6 +- homeassistant/components/openevse/sensor.py | 16 ++-- homeassistant/components/qnap/sensor.py | 47 ++++++++---- 8 files changed, 143 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 7c5557757ef..2f93196a4bb 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SENSORS, CONF_TYPE, + DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, TEMP_CELSIUS, @@ -174,6 +175,7 @@ class LaCrosseSensor(SensorEntity): class LaCrosseTemperature(LaCrosseSensor): """Implementation of a Lacrosse temperature sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_unit_of_measurement = TEMP_CELSIUS @property diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index f03448fa3a9..5dffab65d75 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -12,6 +12,9 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SHOW_ON_MAP, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, @@ -45,19 +48,41 @@ SENSOR_TEMPERATURE = "temperature" TOPIC_UPDATE = f"{DOMAIN}_data_update" SENSORS = { - SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], - SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", PERCENTAGE], - SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", PRESSURE_HPA], - SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", PRESSURE_HPA], + SENSOR_TEMPERATURE: [ + "Temperature", + "mdi:thermometer", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + ], + SENSOR_HUMIDITY: [ + "Humidity", + "mdi:water-percent", + PERCENTAGE, + DEVICE_CLASS_HUMIDITY, + ], + SENSOR_PRESSURE: [ + "Pressure", + "mdi:arrow-down-bold", + PRESSURE_HPA, + DEVICE_CLASS_PRESSURE, + ], + SENSOR_PRESSURE_AT_SEALEVEL: [ + "Pressure at sealevel", + "mdi:download", + PRESSURE_HPA, + DEVICE_CLASS_PRESSURE, + ], SENSOR_PM10: [ "PM10", "mdi:thought-bubble", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + None, ], SENSOR_PM2_5: [ "PM2.5", "mdi:thought-bubble-outline", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + None, ], } diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index aec77961b94..b27cc35ab26 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -31,14 +31,20 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors = [] for sensor_type in luftdaten.sensor_conditions: try: - name, icon, unit = SENSORS[sensor_type] + name, icon, unit, device_class = SENSORS[sensor_type] except KeyError: _LOGGER.debug("Unknown sensor value type: %s", sensor_type) continue sensors.append( LuftdatenSensor( - luftdaten, sensor_type, name, icon, unit, entry.data[CONF_SHOW_ON_MAP] + luftdaten, + sensor_type, + name, + icon, + unit, + device_class, + entry.data[CONF_SHOW_ON_MAP], ) ) @@ -48,7 +54,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class LuftdatenSensor(SensorEntity): """Implementation of a Luftdaten sensor.""" - def __init__(self, luftdaten, sensor_type, name, icon, unit, show): + def __init__(self, luftdaten, sensor_type, name, icon, unit, device_class, show): """Initialize the Luftdaten sensor.""" self._async_unsub_dispatcher_connect = None self.luftdaten = luftdaten @@ -59,6 +65,7 @@ class LuftdatenSensor(SensorEntity): self._unit_of_measurement = unit self._show_on_map = show self._attrs = {} + self._attr_device_class = device_class @property def icon(self): diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index c7a64f17bd6..fafaf53ff99 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + DEVICE_CLASS_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS, @@ -83,7 +84,7 @@ class MfiSensor(SensorEntity): @property def name(self): - """Return the name of th sensor.""" + """Return the name of the sensor.""" return self._port.label @property @@ -100,6 +101,19 @@ class MfiSensor(SensorEntity): digits = DIGITS.get(self._port.tag, 0) return round(self._port.value, digits) + @property + def device_class(self): + """Return the device class of the sensor.""" + try: + tag = self._port.tag + except ValueError: + return None + + if tag == "temperature": + return DEVICE_CLASS_TEMPERATURE + + return None + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 2ede5e38c6a..6a5c80e1e8a 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -9,6 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONDUCTIVITY, DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, ELECTRICAL_CURRENT_AMPERE, ELECTRICAL_VOLT_AMPERE, ENERGY_KILO_WATT_HOUR, @@ -31,37 +33,37 @@ from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { - "V_TEMP": [None, "mdi:thermometer"], - "V_HUM": [PERCENTAGE, "mdi:water-percent"], - "V_DIMMER": [PERCENTAGE, "mdi:percent"], - "V_PERCENTAGE": [PERCENTAGE, "mdi:percent"], - "V_PRESSURE": [None, "mdi:gauge"], - "V_FORECAST": [None, "mdi:weather-partly-cloudy"], - "V_RAIN": [None, "mdi:weather-rainy"], - "V_RAINRATE": [None, "mdi:weather-rainy"], - "V_WIND": [None, "mdi:weather-windy"], - "V_GUST": [None, "mdi:weather-windy"], - "V_DIRECTION": [DEGREE, "mdi:compass"], - "V_WEIGHT": [MASS_KILOGRAMS, "mdi:weight-kilogram"], - "V_DISTANCE": [LENGTH_METERS, "mdi:ruler"], - "V_IMPEDANCE": ["ohm", None], - "V_WATT": [POWER_WATT, None], - "V_KWH": [ENERGY_KILO_WATT_HOUR, None], - "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny"], - "V_FLOW": [LENGTH_METERS, "mdi:gauge"], - "V_VOLUME": [f"{VOLUME_CUBIC_METERS}", None], + "V_TEMP": [None, None, DEVICE_CLASS_TEMPERATURE], + "V_HUM": [PERCENTAGE, "mdi:water-percent", DEVICE_CLASS_HUMIDITY], + "V_DIMMER": [PERCENTAGE, "mdi:percent", None], + "V_PERCENTAGE": [PERCENTAGE, "mdi:percent", None], + "V_PRESSURE": [None, "mdi:gauge", None], + "V_FORECAST": [None, "mdi:weather-partly-cloudy", None], + "V_RAIN": [None, "mdi:weather-rainy", None], + "V_RAINRATE": [None, "mdi:weather-rainy", None], + "V_WIND": [None, "mdi:weather-windy", None], + "V_GUST": [None, "mdi:weather-windy", None], + "V_DIRECTION": [DEGREE, "mdi:compass", None], + "V_WEIGHT": [MASS_KILOGRAMS, "mdi:weight-kilogram", None], + "V_DISTANCE": [LENGTH_METERS, "mdi:ruler", None], + "V_IMPEDANCE": ["ohm", None, None], + "V_WATT": [POWER_WATT, None, None], + "V_KWH": [ENERGY_KILO_WATT_HOUR, None, None], + "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny", None], + "V_FLOW": [LENGTH_METERS, "mdi:gauge", None], + "V_VOLUME": [f"{VOLUME_CUBIC_METERS}", None, None], "V_LEVEL": { - "S_SOUND": ["dB", "mdi:volume-high"], - "S_VIBRATION": [FREQUENCY_HERTZ, None], - "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny"], + "S_SOUND": ["dB", "mdi:volume-high", None], + "S_VIBRATION": [FREQUENCY_HERTZ, None, None], + "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny", None], }, - "V_VOLTAGE": [VOLT, "mdi:flash"], - "V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto"], - "V_PH": ["pH", None], - "V_ORP": ["mV", None], - "V_EC": [CONDUCTIVITY, None], - "V_VAR": ["var", None], - "V_VA": [ELECTRICAL_VOLT_AMPERE, None], + "V_VOLTAGE": [VOLT, "mdi:flash", None], + "V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto", None], + "V_PH": ["pH", None, None], + "V_ORP": ["mV", None, None], + "V_EC": [CONDUCTIVITY, None, None], + "V_VAR": ["var", None, None], + "V_VA": [ELECTRICAL_VOLT_AMPERE, None, None], } @@ -107,9 +109,15 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): @property def state(self) -> str | None: - """Return the state of the device.""" + """Return the state of this entity.""" return self._values.get(self.value_type) + @property + def device_class(self) -> str | None: + """Return the device class of this entity.""" + icon = self._get_sensor_type()[2] + return icon + @property def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" @@ -140,9 +148,11 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq - _sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) + _sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None, None]) if isinstance(_sensor_type, dict): - sensor_type = _sensor_type.get(pres(self.child_type).name, [None, None]) + sensor_type = _sensor_type.get( + pres(self.child_type).name, [None, None, None] + ) else: sensor_type = _sensor_type return sensor_type diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 659b58e9815..48b9a25f783 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,7 +1,7 @@ """Support for Notion sensors.""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -9,7 +9,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NotionEntity from .const import DATA_COORDINATOR, DOMAIN, LOGGER, SENSOR_TEMPERATURE -SENSOR_TYPES = {SENSOR_TEMPERATURE: ("Temperature", "temperature", TEMP_CELSIUS)} +SENSOR_TYPES = { + SENSOR_TEMPERATURE: ("Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) +} async def async_setup_entry( diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index d7d4149e26d..29eeceb232c 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, + DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, TIME_MINUTES, @@ -18,13 +19,13 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "status": ["Charging Status", None], - "charge_time": ["Charge Time Elapsed", TIME_MINUTES], - "ambient_temp": ["Ambient Temperature", TEMP_CELSIUS], - "ir_temp": ["IR Temperature", TEMP_CELSIUS], - "rtc_temp": ["RTC Temperature", TEMP_CELSIUS], - "usage_session": ["Usage this Session", ENERGY_KILO_WATT_HOUR], - "usage_total": ["Total Usage", ENERGY_KILO_WATT_HOUR], + "status": ["Charging Status", None, None], + "charge_time": ["Charge Time Elapsed", TIME_MINUTES, None], + "ambient_temp": ["Ambient Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], + "ir_temp": ["IR Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], + "rtc_temp": ["RTC Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], + "usage_session": ["Usage this Session", ENERGY_KILO_WATT_HOUR, None], + "usage_total": ["Total Usage", ENERGY_KILO_WATT_HOUR, None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -61,6 +62,7 @@ class OpenEVSESensor(SensorEntity): self._state = None self.charger = charger self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property def name(self): diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 5759713e80c..c175d89f60e 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, DATA_GIBIBYTES, DATA_RATE_MEBIBYTES_PER_SECOND, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -56,31 +57,46 @@ NOTIFICATION_ID = "qnap_notification" NOTIFICATION_TITLE = "QNAP Sensor Setup" _SYSTEM_MON_COND = { - "status": ["Status", None, "mdi:checkbox-marked-circle-outline"], - "system_temp": ["System Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "status": ["Status", None, "mdi:checkbox-marked-circle-outline", None], + "system_temp": ["System Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], } _CPU_MON_COND = { - "cpu_temp": ["CPU Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "cpu_usage": ["CPU Usage", PERCENTAGE, "mdi:chip"], + "cpu_temp": ["CPU Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + "cpu_usage": ["CPU Usage", PERCENTAGE, "mdi:chip", None], } _MEMORY_MON_COND = { - "memory_free": ["Memory Available", DATA_GIBIBYTES, "mdi:memory"], - "memory_used": ["Memory Used", DATA_GIBIBYTES, "mdi:memory"], - "memory_percent_used": ["Memory Usage", PERCENTAGE, "mdi:memory"], + "memory_free": ["Memory Available", DATA_GIBIBYTES, "mdi:memory", None], + "memory_used": ["Memory Used", DATA_GIBIBYTES, "mdi:memory", None], + "memory_percent_used": ["Memory Usage", PERCENTAGE, "mdi:memory", None], } _NETWORK_MON_COND = { - "network_link_status": ["Network Link", None, "mdi:checkbox-marked-circle-outline"], - "network_tx": ["Network Up", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:upload"], - "network_rx": ["Network Down", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:download"], + "network_link_status": [ + "Network Link", + None, + "mdi:checkbox-marked-circle-outline", + None, + ], + "network_tx": ["Network Up", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:upload", None], + "network_rx": [ + "Network Down", + DATA_RATE_MEBIBYTES_PER_SECOND, + "mdi:download", + None, + ], } _DRIVE_MON_COND = { - "drive_smart_status": ["SMART Status", None, "mdi:checkbox-marked-circle-outline"], - "drive_temp": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "drive_smart_status": [ + "SMART Status", + None, + "mdi:checkbox-marked-circle-outline", + None, + ], + "drive_temp": ["Temperature", TEMP_CELSIUS, None, None, DEVICE_CLASS_TEMPERATURE], } _VOLUME_MON_COND = { - "volume_size_used": ["Used Space", DATA_GIBIBYTES, "mdi:chart-pie"], - "volume_size_free": ["Free Space", DATA_GIBIBYTES, "mdi:chart-pie"], - "volume_percentage_used": ["Volume Used", PERCENTAGE, "mdi:chart-pie"], + "volume_size_used": ["Used Space", DATA_GIBIBYTES, "mdi:chart-pie", None], + "volume_size_free": ["Free Space", DATA_GIBIBYTES, "mdi:chart-pie", None], + "volume_percentage_used": ["Volume Used", PERCENTAGE, "mdi:chart-pie", None], } _MONITORED_CONDITIONS = ( @@ -210,6 +226,7 @@ class QNAPSensor(SensorEntity): self.var_icon = variable_info[2] self.monitor_device = monitor_device self._api = api + self._attr_device_class = variable_info[3] @property def name(self): From ee52706f03fdcdd9e8485a22df3257c0528cf133 Mon Sep 17 00:00:00 2001 From: bwduncan Date: Mon, 12 Jul 2021 19:57:08 +0100 Subject: [PATCH 232/818] Poll Nissan servers for battery updates (#44826) --- .../components/nissan_leaf/__init__.py | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 24adf223719..a0a39542a20 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -45,7 +45,7 @@ DEFAULT_CLIMATE_INTERVAL = timedelta(minutes=5) RESTRICTED_BATTERY = 2 RESTRICTED_INTERVAL = timedelta(hours=12) -MAX_RESPONSE_ATTEMPTS = 10 +MAX_RESPONSE_ATTEMPTS = 3 PYCARWINGS2_SLEEP = 30 @@ -194,6 +194,14 @@ def setup(hass, config): return True +def _extract_start_date(battery_info): + """Extract the server date from the battery response.""" + try: + return battery_info.answer["BatteryStatusRecords"]["OperationDateAndTime"] + except KeyError: + return None + + class LeafDataStore: """Nissan Leaf Data Store.""" @@ -324,19 +332,24 @@ class LeafDataStore: self.request_in_progress = False async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) - @staticmethod - def _extract_start_date(battery_info): - """Extract the server date from the battery response.""" - try: - return battery_info.answer["BatteryStatusRecords"]["OperationDateAndTime"] - except KeyError: - return None - async def async_get_battery(self): """Request battery update from Nissan servers.""" try: # Request battery update from the car _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) + start_date = None + try: + start_server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) + except TypeError: # pycarwings2 can fail if Nissan returns nothing + _LOGGER.debug("Battery status check returned nothing") + else: + if not start_server_info: + _LOGGER.debug("Battery status check failed") + else: + start_date = _extract_start_date(start_server_info) + await asyncio.sleep(1) # Critical sleep request = await self.hass.async_add_executor_job(self.leaf.request_update) if not request: _LOGGER.error("Battery update request failed") @@ -364,7 +377,19 @@ class LeafDataStore: server_info = await self.hass.async_add_executor_job( self.leaf.get_latest_battery_status ) - return server_info + if not start_date or ( + server_info and start_date != _extract_start_date(server_info) + ): + return server_info + # get_status_from_update returned {"resultFlag": "1"} + # but the data didn't change, make a fresh request. + await asyncio.sleep(1) # Critical sleep + request = await self.hass.async_add_executor_job( + self.leaf.request_update + ) + if not request: + _LOGGER.error("Battery update request failed") + return None _LOGGER.debug( "%s attempts exceeded return latest data from server", @@ -379,7 +404,7 @@ class LeafDataStore: except CarwingsError: _LOGGER.error("An error occurred getting battery status") return None - except KeyError: + except (KeyError, TypeError): _LOGGER.error("An error occurred parsing response from server") return None From e6ea9f470854b586a7fbd0fed7c724d58a12b800 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 09:39:51 -1000 Subject: [PATCH 233/818] Fix nexia thermostats humidify without dehumidify support (#52758) --- .coveragerc | 1 + homeassistant/components/nexia/climate.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index d7eae65becf..8db2deee2e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -687,6 +687,7 @@ omit = homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py + homeassistant/components/nexia/climate.py homeassistant/components/nextcloud/* homeassistant/components/nfandroidtv/notify.py homeassistant/components/niko_home_control/light.py diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 2dff498f281..e27a1816a8e 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -203,7 +203,10 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidity(self, humidity): """Dehumidify target.""" - self._thermostat.set_dehumidify_setpoint(humidity / 100.0) + if self._thermostat.has_dehumidify_support(): + self._thermostat.set_dehumidify_setpoint(humidity / 100.0) + else: + self._thermostat.set_humidify_setpoint(humidity / 100.0) self._signal_thermostat_update() @property From 0099b5448960cd596329d7ccb08318486698f11c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 10:03:13 -1000 Subject: [PATCH 234/818] Fix recorder purge with sqlite3 < 3.32.0 (#52929) --- homeassistant/components/recorder/const.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 026628a32df..eab3c30e99e 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -7,4 +7,9 @@ DOMAIN = "recorder" CONF_DB_INTEGRITY_CHECK = "db_integrity_check" # The maximum number of rows (events) we purge in one delete statement -MAX_ROWS_TO_PURGE = 1000 + +# sqlite3 has a limit of 999 until version 3.32.0 +# in https://github.com/sqlite/sqlite/commit/efdba1a8b3c6c967e7fae9c1989c40d420ce64cc +# We can increase this back to 1000 once most +# have upgraded their sqlite version +MAX_ROWS_TO_PURGE = 998 From 9b8a77600192287f35a6a4772eba0c90c449708f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Jul 2021 22:45:29 +0200 Subject: [PATCH 235/818] Set device_class on temperature sensors A-E (#49524) Co-authored-by: Franck Nijhof --- homeassistant/components/apcupsd/sensor.py | 134 +++---- homeassistant/components/aqualogic/sensor.py | 36 +- homeassistant/components/arwn/sensor.py | 19 +- homeassistant/components/bloomsky/sensor.py | 11 + homeassistant/components/buienradar/sensor.py | 340 ++++++++++++------ homeassistant/components/ebusd/const.py | 219 ++++++++--- homeassistant/components/ebusd/sensor.py | 13 +- .../components/ecoal_boiler/sensor.py | 7 +- .../eddystone_temperature/sensor.py | 6 + .../components/eight_sleep/sensor.py | 20 +- .../components/environment_canada/sensor.py | 8 + homeassistant/components/envirophat/sensor.py | 38 +- 12 files changed, 593 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 190a3f5f1d8..4748ae2476e 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_RESOURCES, + DEVICE_CLASS_TEMPERATURE, ELECTRICAL_CURRENT_AMPERE, ELECTRICAL_VOLT_AMPERE, FREQUENCY_HERTZ, @@ -25,72 +26,72 @@ _LOGGER = logging.getLogger(__name__) SENSOR_PREFIX = "UPS " SENSOR_TYPES = { - "alarmdel": ["Alarm Delay", "", "mdi:alarm"], - "ambtemp": ["Ambient Temperature", "", "mdi:thermometer"], - "apc": ["Status Data", "", "mdi:information-outline"], - "apcmodel": ["Model", "", "mdi:information-outline"], - "badbatts": ["Bad Batteries", "", "mdi:information-outline"], - "battdate": ["Battery Replaced", "", "mdi:calendar-clock"], - "battstat": ["Battery Status", "", "mdi:information-outline"], - "battv": ["Battery Voltage", VOLT, "mdi:flash"], - "bcharge": ["Battery", PERCENTAGE, "mdi:battery"], - "cable": ["Cable Type", "", "mdi:ethernet-cable"], - "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline"], - "date": ["Status Date", "", "mdi:calendar-clock"], - "dipsw": ["Dip Switch Settings", "", "mdi:information-outline"], - "dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert"], - "driver": ["Driver", "", "mdi:information-outline"], - "dshutd": ["Shutdown Delay", "", "mdi:timer-outline"], - "dwake": ["Wake Delay", "", "mdi:timer-outline"], - "endapc": ["Date and Time", "", "mdi:calendar-clock"], - "extbatts": ["External Batteries", "", "mdi:information-outline"], - "firmware": ["Firmware Version", "", "mdi:information-outline"], - "hitrans": ["Transfer High", VOLT, "mdi:flash"], - "hostname": ["Hostname", "", "mdi:information-outline"], - "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent"], - "itemp": ["Internal Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "lastxfer": ["Last Transfer", "", "mdi:transfer"], - "linefail": ["Input Voltage Status", "", "mdi:information-outline"], - "linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline"], - "linev": ["Input Voltage", VOLT, "mdi:flash"], - "loadpct": ["Load", PERCENTAGE, "mdi:gauge"], - "loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge"], - "lotrans": ["Transfer Low", VOLT, "mdi:flash"], - "mandate": ["Manufacture Date", "", "mdi:calendar"], - "masterupd": ["Master Update", "", "mdi:information-outline"], - "maxlinev": ["Input Voltage High", VOLT, "mdi:flash"], - "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline"], - "mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert"], - "minlinev": ["Input Voltage Low", VOLT, "mdi:flash"], - "mintimel": ["Shutdown Time", "", "mdi:timer-outline"], - "model": ["Model", "", "mdi:information-outline"], - "nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash"], - "nominv": ["Nominal Input Voltage", VOLT, "mdi:flash"], - "nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash"], - "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash"], - "nomapnt": ["Nominal Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash"], - "numxfers": ["Transfer Count", "", "mdi:counter"], - "outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash"], - "outputv": ["Output Voltage", VOLT, "mdi:flash"], - "reg1": ["Register 1 Fault", "", "mdi:information-outline"], - "reg2": ["Register 2 Fault", "", "mdi:information-outline"], - "reg3": ["Register 3 Fault", "", "mdi:information-outline"], - "retpct": ["Restore Requirement", PERCENTAGE, "mdi:battery-alert"], - "selftest": ["Last Self Test", "", "mdi:calendar-clock"], - "sense": ["Sensitivity", "", "mdi:information-outline"], - "serialno": ["Serial Number", "", "mdi:information-outline"], - "starttime": ["Startup Time", "", "mdi:calendar-clock"], - "statflag": ["Status Flag", "", "mdi:information-outline"], - "status": ["Status", "", "mdi:information-outline"], - "stesti": ["Self Test Interval", "", "mdi:information-outline"], - "timeleft": ["Time Left", "", "mdi:clock-alert"], - "tonbatt": ["Time on Battery", "", "mdi:timer-outline"], - "upsmode": ["Mode", "", "mdi:information-outline"], - "upsname": ["Name", "", "mdi:information-outline"], - "version": ["Daemon Info", "", "mdi:information-outline"], - "xoffbat": ["Transfer from Battery", "", "mdi:transfer"], - "xoffbatt": ["Transfer from Battery", "", "mdi:transfer"], - "xonbatt": ["Transfer to Battery", "", "mdi:transfer"], + "alarmdel": ["Alarm Delay", "", "mdi:alarm", None], + "ambtemp": ["Ambient Temperature", "", "mdi:thermometer", None], + "apc": ["Status Data", "", "mdi:information-outline", None], + "apcmodel": ["Model", "", "mdi:information-outline", None], + "badbatts": ["Bad Batteries", "", "mdi:information-outline", None], + "battdate": ["Battery Replaced", "", "mdi:calendar-clock", None], + "battstat": ["Battery Status", "", "mdi:information-outline", None], + "battv": ["Battery Voltage", VOLT, "mdi:flash", None], + "bcharge": ["Battery", PERCENTAGE, "mdi:battery", None], + "cable": ["Cable Type", "", "mdi:ethernet-cable", None], + "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline", None], + "date": ["Status Date", "", "mdi:calendar-clock", None], + "dipsw": ["Dip Switch Settings", "", "mdi:information-outline", None], + "dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert", None], + "driver": ["Driver", "", "mdi:information-outline", None], + "dshutd": ["Shutdown Delay", "", "mdi:timer-outline", None], + "dwake": ["Wake Delay", "", "mdi:timer-outline", None], + "endapc": ["Date and Time", "", "mdi:calendar-clock", None], + "extbatts": ["External Batteries", "", "mdi:information-outline", None], + "firmware": ["Firmware Version", "", "mdi:information-outline", None], + "hitrans": ["Transfer High", VOLT, "mdi:flash", None], + "hostname": ["Hostname", "", "mdi:information-outline", None], + "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None], + "itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + "lastxfer": ["Last Transfer", "", "mdi:transfer", None], + "linefail": ["Input Voltage Status", "", "mdi:information-outline", None], + "linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline", None], + "linev": ["Input Voltage", VOLT, "mdi:flash", None], + "loadpct": ["Load", PERCENTAGE, "mdi:gauge", None], + "loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge", None], + "lotrans": ["Transfer Low", VOLT, "mdi:flash", None], + "mandate": ["Manufacture Date", "", "mdi:calendar", None], + "masterupd": ["Master Update", "", "mdi:information-outline", None], + "maxlinev": ["Input Voltage High", VOLT, "mdi:flash", None], + "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline", None], + "mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert", None], + "minlinev": ["Input Voltage Low", VOLT, "mdi:flash", None], + "mintimel": ["Shutdown Time", "", "mdi:timer-outline", None], + "model": ["Model", "", "mdi:information-outline", None], + "nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash", None], + "nominv": ["Nominal Input Voltage", VOLT, "mdi:flash", None], + "nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash", None], + "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash", None], + "nomapnt": ["Nominal Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], + "numxfers": ["Transfer Count", "", "mdi:counter", None], + "outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash", None], + "outputv": ["Output Voltage", VOLT, "mdi:flash", None], + "reg1": ["Register 1 Fault", "", "mdi:information-outline", None], + "reg2": ["Register 2 Fault", "", "mdi:information-outline", None], + "reg3": ["Register 3 Fault", "", "mdi:information-outline", None], + "retpct": ["Restore Requirement", PERCENTAGE, "mdi:battery-alert", None], + "selftest": ["Last Self Test", "", "mdi:calendar-clock", None], + "sense": ["Sensitivity", "", "mdi:information-outline", None], + "serialno": ["Serial Number", "", "mdi:information-outline", None], + "starttime": ["Startup Time", "", "mdi:calendar-clock", None], + "statflag": ["Status Flag", "", "mdi:information-outline", None], + "status": ["Status", "", "mdi:information-outline", None], + "stesti": ["Self Test Interval", "", "mdi:information-outline", None], + "timeleft": ["Time Left", "", "mdi:clock-alert", None], + "tonbatt": ["Time on Battery", "", "mdi:timer-outline", None], + "upsmode": ["Mode", "", "mdi:information-outline", None], + "upsname": ["Name", "", "mdi:information-outline", None], + "version": ["Daemon Info", "", "mdi:information-outline", None], + "xoffbat": ["Transfer from Battery", "", "mdi:transfer", None], + "xoffbatt": ["Transfer from Battery", "", "mdi:transfer", None], + "xonbatt": ["Transfer to Battery", "", "mdi:transfer", None], } SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} @@ -166,6 +167,7 @@ class APCUPSdSensor(SensorEntity): self._attr_icon = SENSOR_TYPES[self.type][2] if SENSOR_TYPES[sensor_type][1]: self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_device_class = SENSOR_TYPES[sensor_type][3] def update(self): """Get the latest status and use it to update our sensor state.""" diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 315b039f778..1608f6173d8 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, @@ -21,18 +22,28 @@ SALT_UNITS = ["g/L", "PPM"] WATT_UNITS = [POWER_WATT, POWER_WATT] NO_UNITS = [None, None] -# sensor_type [ description, unit, icon ] +# sensor_type [ description, unit, icon, device_class ] # sensor_type corresponds to property names in aqualogic.core.AquaLogic SENSOR_TYPES = { - "air_temp": ["Air Temperature", TEMP_UNITS, "mdi:thermometer"], - "pool_temp": ["Pool Temperature", TEMP_UNITS, "mdi:oil-temperature"], - "spa_temp": ["Spa Temperature", TEMP_UNITS, "mdi:oil-temperature"], - "pool_chlorinator": ["Pool Chlorinator", PERCENT_UNITS, "mdi:gauge"], - "spa_chlorinator": ["Spa Chlorinator", PERCENT_UNITS, "mdi:gauge"], - "salt_level": ["Salt Level", SALT_UNITS, "mdi:gauge"], - "pump_speed": ["Pump Speed", PERCENT_UNITS, "mdi:speedometer"], - "pump_power": ["Pump Power", WATT_UNITS, "mdi:gauge"], - "status": ["Status", NO_UNITS, "mdi:alert"], + "air_temp": ["Air Temperature", TEMP_UNITS, None, DEVICE_CLASS_TEMPERATURE], + "pool_temp": [ + "Pool Temperature", + TEMP_UNITS, + "mdi:oil-temperature", + DEVICE_CLASS_TEMPERATURE, + ], + "spa_temp": [ + "Spa Temperature", + TEMP_UNITS, + "mdi:oil-temperature", + DEVICE_CLASS_TEMPERATURE, + ], + "pool_chlorinator": ["Pool Chlorinator", PERCENT_UNITS, "mdi:gauge", None], + "spa_chlorinator": ["Spa Chlorinator", PERCENT_UNITS, "mdi:gauge", None], + "salt_level": ["Salt Level", SALT_UNITS, "mdi:gauge", None], + "pump_speed": ["Pump Speed", PERCENT_UNITS, "mdi:speedometer", None], + "pump_power": ["Pump Power", WATT_UNITS, "mdi:gauge", None], + "status": ["Status", NO_UNITS, "mdi:alert", None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -89,6 +100,11 @@ class AquaLogicSensor(SensorEntity): """Return the polling state.""" return False + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES[self._type][3] + @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index ba9166d1af5..83ff7da1b56 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -4,7 +4,12 @@ import logging from homeassistant.components import mqtt from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEGREE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import callback from homeassistant.util import slugify @@ -30,7 +35,9 @@ def discover_sensors(topic, payload): unit = TEMP_FAHRENHEIT else: unit = TEMP_CELSIUS - return ArwnSensor(topic, name, "temp", unit) + return ArwnSensor( + topic, name, "temp", unit, device_class=DEVICE_CLASS_TEMPERATURE + ) if domain == "moisture": name = f"{parts[2]} Moisture" return ArwnSensor(topic, name, "moisture", unit, "mdi:water-percent") @@ -117,7 +124,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class ArwnSensor(SensorEntity): """Representation of an ARWN sensor.""" - def __init__(self, topic, name, state_key, units, icon=None): + def __init__(self, topic, name, state_key, units, icon=None, device_class=None): """Initialize the sensor.""" self.hass = None self.entity_id = _slug(name) @@ -128,6 +135,7 @@ class ArwnSensor(SensorEntity): self.event = {} self._unit_of_measurement = units self._icon = icon + self._device_class = device_class def set_event(self, event): """Update the sensor with the most recent event.""" @@ -168,6 +176,11 @@ class ArwnSensor(SensorEntity): """Return the polling state.""" return False + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._device_class + @property def icon(self): """Return the icon of device based on its type.""" diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 4dc52e1a85c..cf494c40916 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( AREA_SQUARE_METERS, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_INHG, PRESSURE_MBAR, @@ -43,6 +44,11 @@ SENSOR_UNITS_METRIC = { "Voltage": "mV", } +# Device class +SENSOR_DEVICE_CLASS = { + "Temperature": DEVICE_CLASS_TEMPERATURE, +} + # Which sensors to format numerically FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"] @@ -103,6 +109,11 @@ class BloomSkySensor(SensorEntity): return SENSOR_UNITS_METRIC.get(self._sensor_name, None) return SENSOR_UNITS_IMPERIAL.get(self._sensor_name, None) + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_DEVICE_CLASS.get(self._sensor_name) + def update(self): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index e4a317cface..10507166288 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, DEGREE, + DEVICE_CLASS_TEMPERATURE, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, LENGTH_MILLIMETERS, @@ -60,132 +61,260 @@ SCHEDULE_NOK = 2 # Supported sensor types: # Key: ['label', unit, icon] SENSOR_TYPES = { - "stationname": ["Stationname", None, None], + "stationname": ["Stationname", None, None, None], # new in json api (>1.0.0): - "barometerfc": ["Barometer value", None, "mdi:gauge"], + "barometerfc": ["Barometer value", None, "mdi:gauge", None], # new in json api (>1.0.0): - "barometerfcname": ["Barometer", None, "mdi:gauge"], + "barometerfcname": ["Barometer", None, "mdi:gauge", None], # new in json api (>1.0.0): - "barometerfcnamenl": ["Barometer", None, "mdi:gauge"], - "condition": ["Condition", None, None], - "conditioncode": ["Condition code", None, None], - "conditiondetailed": ["Detailed condition", None, None], - "conditionexact": ["Full condition", None, None], - "symbol": ["Symbol", None, None], + "barometerfcnamenl": ["Barometer", None, "mdi:gauge", None], + "condition": ["Condition", None, None, None], + "conditioncode": ["Condition code", None, None, None], + "conditiondetailed": ["Detailed condition", None, None, None], + "conditionexact": ["Full condition", None, None, None], + "symbol": ["Symbol", None, None, None], # new in json api (>1.0.0): - "feeltemperature": ["Feel temperature", TEMP_CELSIUS, "mdi:thermometer"], - "humidity": ["Humidity", PERCENTAGE, "mdi:water-percent"], - "temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "groundtemperature": ["Ground temperature", TEMP_CELSIUS, "mdi:thermometer"], - "windspeed": ["Wind speed", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "windforce": ["Wind force", "Bft", "mdi:weather-windy"], - "winddirection": ["Wind direction", None, "mdi:compass-outline"], - "windazimuth": ["Wind direction azimuth", DEGREE, "mdi:compass-outline"], - "pressure": ["Pressure", PRESSURE_HPA, "mdi:gauge"], - "visibility": ["Visibility", LENGTH_KILOMETERS, None], - "windgust": ["Wind gust", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], + "feeltemperature": [ + "Feel temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "humidity": ["Humidity", PERCENTAGE, "mdi:water-percent", None], + "temperature": [ + "Temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "groundtemperature": [ + "Ground temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "windspeed": ["Wind speed", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy", None], + "windforce": ["Wind force", "Bft", "mdi:weather-windy", None], + "winddirection": ["Wind direction", None, "mdi:compass-outline", None], + "windazimuth": ["Wind direction azimuth", DEGREE, "mdi:compass-outline", None], + "pressure": ["Pressure", PRESSURE_HPA, "mdi:gauge", None], + "visibility": ["Visibility", LENGTH_KILOMETERS, None, None], + "windgust": ["Wind gust", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy", None], "precipitation": [ "Precipitation", PRECIPITATION_MILLIMETERS_PER_HOUR, "mdi:weather-pouring", + None, + ], + "irradiance": [ + "Irradiance", + IRRADIATION_WATTS_PER_SQUARE_METER, + "mdi:sunglasses", + None, ], - "irradiance": ["Irradiance", IRRADIATION_WATTS_PER_SQUARE_METER, "mdi:sunglasses"], "precipitation_forecast_average": [ "Precipitation forecast average", PRECIPITATION_MILLIMETERS_PER_HOUR, "mdi:weather-pouring", + None, ], "precipitation_forecast_total": [ "Precipitation forecast total", LENGTH_MILLIMETERS, "mdi:weather-pouring", + None, ], # new in json api (>1.0.0): - "rainlast24hour": ["Rain last 24h", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "rainlast24hour": [ + "Rain last 24h", + LENGTH_MILLIMETERS, + "mdi:weather-pouring", + None, + ], # new in json api (>1.0.0): - "rainlasthour": ["Rain last hour", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "temperature_1d": ["Temperature 1d", TEMP_CELSIUS, "mdi:thermometer"], - "temperature_2d": ["Temperature 2d", TEMP_CELSIUS, "mdi:thermometer"], - "temperature_3d": ["Temperature 3d", TEMP_CELSIUS, "mdi:thermometer"], - "temperature_4d": ["Temperature 4d", TEMP_CELSIUS, "mdi:thermometer"], - "temperature_5d": ["Temperature 5d", TEMP_CELSIUS, "mdi:thermometer"], - "mintemp_1d": ["Minimum temperature 1d", TEMP_CELSIUS, "mdi:thermometer"], - "mintemp_2d": ["Minimum temperature 2d", TEMP_CELSIUS, "mdi:thermometer"], - "mintemp_3d": ["Minimum temperature 3d", TEMP_CELSIUS, "mdi:thermometer"], - "mintemp_4d": ["Minimum temperature 4d", TEMP_CELSIUS, "mdi:thermometer"], - "mintemp_5d": ["Minimum temperature 5d", TEMP_CELSIUS, "mdi:thermometer"], - "rain_1d": ["Rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "rain_2d": ["Rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "rain_3d": ["Rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "rain_4d": ["Rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "rain_5d": ["Rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "rainlasthour": ["Rain last hour", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "temperature_1d": [ + "Temperature 1d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "temperature_2d": [ + "Temperature 2d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "temperature_3d": [ + "Temperature 3d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "temperature_4d": [ + "Temperature 4d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "temperature_5d": [ + "Temperature 5d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "mintemp_1d": [ + "Minimum temperature 1d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "mintemp_2d": [ + "Minimum temperature 2d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "mintemp_3d": [ + "Minimum temperature 3d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "mintemp_4d": [ + "Minimum temperature 4d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "mintemp_5d": [ + "Minimum temperature 5d", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "rain_1d": ["Rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "rain_2d": ["Rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "rain_3d": ["Rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "rain_4d": ["Rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "rain_5d": ["Rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], # new in json api (>1.0.0): - "minrain_1d": ["Minimum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "minrain_2d": ["Minimum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "minrain_3d": ["Minimum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "minrain_4d": ["Minimum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "minrain_5d": ["Minimum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "minrain_1d": ["Minimum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "minrain_2d": ["Minimum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "minrain_3d": ["Minimum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "minrain_4d": ["Minimum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "minrain_5d": ["Minimum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], # new in json api (>1.0.0): - "maxrain_1d": ["Maximum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "maxrain_2d": ["Maximum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "maxrain_3d": ["Maximum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "maxrain_4d": ["Maximum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "maxrain_5d": ["Maximum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], - "rainchance_1d": ["Rainchance 1d", PERCENTAGE, "mdi:weather-pouring"], - "rainchance_2d": ["Rainchance 2d", PERCENTAGE, "mdi:weather-pouring"], - "rainchance_3d": ["Rainchance 3d", PERCENTAGE, "mdi:weather-pouring"], - "rainchance_4d": ["Rainchance 4d", PERCENTAGE, "mdi:weather-pouring"], - "rainchance_5d": ["Rainchance 5d", PERCENTAGE, "mdi:weather-pouring"], - "sunchance_1d": ["Sunchance 1d", PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_2d": ["Sunchance 2d", PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_3d": ["Sunchance 3d", PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_4d": ["Sunchance 4d", PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_5d": ["Sunchance 5d", PERCENTAGE, "mdi:weather-partly-cloudy"], - "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy"], - "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy"], - "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy"], - "windforce_4d": ["Wind force 4d", "Bft", "mdi:weather-windy"], - "windforce_5d": ["Wind force 5d", "Bft", "mdi:weather-windy"], - "windspeed_1d": ["Wind speed 1d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "windspeed_2d": ["Wind speed 2d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "windspeed_3d": ["Wind speed 3d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "windspeed_4d": ["Wind speed 4d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "windspeed_5d": ["Wind speed 5d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "winddirection_1d": ["Wind direction 1d", None, "mdi:compass-outline"], - "winddirection_2d": ["Wind direction 2d", None, "mdi:compass-outline"], - "winddirection_3d": ["Wind direction 3d", None, "mdi:compass-outline"], - "winddirection_4d": ["Wind direction 4d", None, "mdi:compass-outline"], - "winddirection_5d": ["Wind direction 5d", None, "mdi:compass-outline"], - "windazimuth_1d": ["Wind direction azimuth 1d", DEGREE, "mdi:compass-outline"], - "windazimuth_2d": ["Wind direction azimuth 2d", DEGREE, "mdi:compass-outline"], - "windazimuth_3d": ["Wind direction azimuth 3d", DEGREE, "mdi:compass-outline"], - "windazimuth_4d": ["Wind direction azimuth 4d", DEGREE, "mdi:compass-outline"], - "windazimuth_5d": ["Wind direction azimuth 5d", DEGREE, "mdi:compass-outline"], - "condition_1d": ["Condition 1d", None, None], - "condition_2d": ["Condition 2d", None, None], - "condition_3d": ["Condition 3d", None, None], - "condition_4d": ["Condition 4d", None, None], - "condition_5d": ["Condition 5d", None, None], - "conditioncode_1d": ["Condition code 1d", None, None], - "conditioncode_2d": ["Condition code 2d", None, None], - "conditioncode_3d": ["Condition code 3d", None, None], - "conditioncode_4d": ["Condition code 4d", None, None], - "conditioncode_5d": ["Condition code 5d", None, None], - "conditiondetailed_1d": ["Detailed condition 1d", None, None], - "conditiondetailed_2d": ["Detailed condition 2d", None, None], - "conditiondetailed_3d": ["Detailed condition 3d", None, None], - "conditiondetailed_4d": ["Detailed condition 4d", None, None], - "conditiondetailed_5d": ["Detailed condition 5d", None, None], - "conditionexact_1d": ["Full condition 1d", None, None], - "conditionexact_2d": ["Full condition 2d", None, None], - "conditionexact_3d": ["Full condition 3d", None, None], - "conditionexact_4d": ["Full condition 4d", None, None], - "conditionexact_5d": ["Full condition 5d", None, None], - "symbol_1d": ["Symbol 1d", None, None], - "symbol_2d": ["Symbol 2d", None, None], - "symbol_3d": ["Symbol 3d", None, None], - "symbol_4d": ["Symbol 4d", None, None], - "symbol_5d": ["Symbol 5d", None, None], + "maxrain_1d": ["Maximum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "maxrain_2d": ["Maximum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "maxrain_3d": ["Maximum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "maxrain_4d": ["Maximum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "maxrain_5d": ["Maximum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring", None], + "rainchance_1d": ["Rainchance 1d", PERCENTAGE, "mdi:weather-pouring", None], + "rainchance_2d": ["Rainchance 2d", PERCENTAGE, "mdi:weather-pouring", None], + "rainchance_3d": ["Rainchance 3d", PERCENTAGE, "mdi:weather-pouring", None], + "rainchance_4d": ["Rainchance 4d", PERCENTAGE, "mdi:weather-pouring", None], + "rainchance_5d": ["Rainchance 5d", PERCENTAGE, "mdi:weather-pouring", None], + "sunchance_1d": ["Sunchance 1d", PERCENTAGE, "mdi:weather-partly-cloudy", None], + "sunchance_2d": ["Sunchance 2d", PERCENTAGE, "mdi:weather-partly-cloudy", None], + "sunchance_3d": ["Sunchance 3d", PERCENTAGE, "mdi:weather-partly-cloudy", None], + "sunchance_4d": ["Sunchance 4d", PERCENTAGE, "mdi:weather-partly-cloudy", None], + "sunchance_5d": ["Sunchance 5d", PERCENTAGE, "mdi:weather-partly-cloudy", None], + "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy", None], + "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy", None], + "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy", None], + "windforce_4d": ["Wind force 4d", "Bft", "mdi:weather-windy", None], + "windforce_5d": ["Wind force 5d", "Bft", "mdi:weather-windy", None], + "windspeed_1d": [ + "Wind speed 1d", + SPEED_KILOMETERS_PER_HOUR, + "mdi:weather-windy", + None, + ], + "windspeed_2d": [ + "Wind speed 2d", + SPEED_KILOMETERS_PER_HOUR, + "mdi:weather-windy", + None, + ], + "windspeed_3d": [ + "Wind speed 3d", + SPEED_KILOMETERS_PER_HOUR, + "mdi:weather-windy", + None, + ], + "windspeed_4d": [ + "Wind speed 4d", + SPEED_KILOMETERS_PER_HOUR, + "mdi:weather-windy", + None, + ], + "windspeed_5d": [ + "Wind speed 5d", + SPEED_KILOMETERS_PER_HOUR, + "mdi:weather-windy", + None, + ], + "winddirection_1d": ["Wind direction 1d", None, "mdi:compass-outline", None], + "winddirection_2d": ["Wind direction 2d", None, "mdi:compass-outline", None], + "winddirection_3d": ["Wind direction 3d", None, "mdi:compass-outline", None], + "winddirection_4d": ["Wind direction 4d", None, "mdi:compass-outline", None], + "winddirection_5d": ["Wind direction 5d", None, "mdi:compass-outline", None], + "windazimuth_1d": [ + "Wind direction azimuth 1d", + DEGREE, + "mdi:compass-outline", + None, + ], + "windazimuth_2d": [ + "Wind direction azimuth 2d", + DEGREE, + "mdi:compass-outline", + None, + ], + "windazimuth_3d": [ + "Wind direction azimuth 3d", + DEGREE, + "mdi:compass-outline", + None, + ], + "windazimuth_4d": [ + "Wind direction azimuth 4d", + DEGREE, + "mdi:compass-outline", + None, + ], + "windazimuth_5d": [ + "Wind direction azimuth 5d", + DEGREE, + "mdi:compass-outline", + None, + ], + "condition_1d": ["Condition 1d", None, None, None], + "condition_2d": ["Condition 2d", None, None, None], + "condition_3d": ["Condition 3d", None, None, None], + "condition_4d": ["Condition 4d", None, None, None], + "condition_5d": ["Condition 5d", None, None, None], + "conditioncode_1d": ["Condition code 1d", None, None, None], + "conditioncode_2d": ["Condition code 2d", None, None, None], + "conditioncode_3d": ["Condition code 3d", None, None, None], + "conditioncode_4d": ["Condition code 4d", None, None, None], + "conditioncode_5d": ["Condition code 5d", None, None, None], + "conditiondetailed_1d": ["Detailed condition 1d", None, None, None], + "conditiondetailed_2d": ["Detailed condition 2d", None, None, None], + "conditiondetailed_3d": ["Detailed condition 3d", None, None, None], + "conditiondetailed_4d": ["Detailed condition 4d", None, None, None], + "conditiondetailed_5d": ["Detailed condition 5d", None, None, None], + "conditionexact_1d": ["Full condition 1d", None, None, None], + "conditionexact_2d": ["Full condition 2d", None, None, None], + "conditionexact_3d": ["Full condition 3d", None, None, None], + "conditionexact_4d": ["Full condition 4d", None, None, None], + "conditionexact_5d": ["Full condition 5d", None, None, None], + "symbol_1d": ["Symbol 1d", None, None, None], + "symbol_2d": ["Symbol 2d", None, None, None], + "symbol_3d": ["Symbol 3d", None, None, None], + "symbol_4d": ["Symbol 4d", None, None, None], + "symbol_5d": ["Symbol 5d", None, None, None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -471,6 +600,11 @@ class BrSensor(SensorEntity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES[self.type][3] + @property def icon(self): """Return possible sensor specific icon.""" diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index c4ff789202d..7052a9950fd 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,5 +1,6 @@ """Constants for ebus component.""" from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, PRESSURE_BAR, @@ -17,127 +18,243 @@ SENSOR_TYPES = { "ActualFlowTemperatureDesired": [ "Hc1ActualFlowTempDesired", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, ], "MaxFlowTemperatureDesired": [ "Hc1MaxFlowTempDesired", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, ], "MinFlowTemperatureDesired": [ "Hc1MinFlowTempDesired", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, ], - "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2], + "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2, None], "HCSummerTemperatureLimit": [ "Hc1SummerTempLimit", TEMP_CELSIUS, "mdi:weather-sunny", 0, + DEVICE_CLASS_TEMPERATURE, + ], + "HolidayTemperature": [ + "HolidayTemp", + TEMP_CELSIUS, + None, + 0, + DEVICE_CLASS_TEMPERATURE, + ], + "HWTemperatureDesired": [ + "HwcTempDesired", + TEMP_CELSIUS, + None, + 0, + DEVICE_CLASS_TEMPERATURE, + ], + "HWActualTemperature": [ + "HwcStorageTemp", + TEMP_CELSIUS, + None, + 0, + DEVICE_CLASS_TEMPERATURE, + ], + "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1, None], + "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None], + "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None], + "HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1, None], + "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1, None], + "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1, None], + "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1, None], + "HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3, None], + "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:water-pump", 0, None], + "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0, None], + "Zone1NightTemperature": [ + "z1NightTemp", + TEMP_CELSIUS, + "mdi:weather-night", + 0, + DEVICE_CLASS_TEMPERATURE, + ], + "Zone1DayTemperature": [ + "z1DayTemp", + TEMP_CELSIUS, + "mdi:weather-sunny", + 0, + DEVICE_CLASS_TEMPERATURE, ], - "HolidayTemperature": ["HolidayTemp", TEMP_CELSIUS, "mdi:thermometer", 0], - "HWTemperatureDesired": ["HwcTempDesired", TEMP_CELSIUS, "mdi:thermometer", 0], - "HWActualTemperature": ["HwcStorageTemp", TEMP_CELSIUS, "mdi:thermometer", 0], - "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1], - "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1], - "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1], - "HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1], - "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1], - "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1], - "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1], - "HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3], - "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:water-pump", 0], - "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0], - "Zone1NightTemperature": ["z1NightTemp", TEMP_CELSIUS, "mdi:weather-night", 0], - "Zone1DayTemperature": ["z1DayTemp", TEMP_CELSIUS, "mdi:weather-sunny", 0], "Zone1HolidayTemperature": [ "z1HolidayTemp", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, + ], + "Zone1RoomTemperature": [ + "z1RoomTemp", + TEMP_CELSIUS, + None, + 0, + DEVICE_CLASS_TEMPERATURE, ], - "Zone1RoomTemperature": ["z1RoomTemp", TEMP_CELSIUS, "mdi:thermometer", 0], "Zone1ActualRoomTemperatureDesired": [ "z1ActualRoomTempDesired", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, + ], + "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1, None], + "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1, None], + "Zone1TimerWednesday": [ + "z1Timer.Wednesday", + None, + "mdi:timer-outline", + 1, + None, + ], + "Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1, None], + "Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1, None], + "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1, None], + "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1, None], + "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3, None], + "ContinuosHeating": [ + "ContinuosHeating", + TEMP_CELSIUS, + "mdi:weather-snowy", + 0, + DEVICE_CLASS_TEMPERATURE, ], - "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1], - "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1], - "Zone1TimerWednesday": ["z1Timer.Wednesday", None, "mdi:timer-outline", 1], - "Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1], - "Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1], - "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1], - "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1], - "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3], - "ContinuosHeating": ["ContinuosHeating", TEMP_CELSIUS, "mdi:weather-snowy", 0], "PowerEnergyConsumptionLastMonth": [ "PrEnergySumHcLastMonth", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0, + None, ], "PowerEnergyConsumptionThisMonth": [ "PrEnergySumHcThisMonth", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0, + None, ], }, "ehp": { - "HWTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], - "OutsideTemp": ["OutsideTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "HWTemperature": ["HwcTemp", TEMP_CELSIUS, None, 4, DEVICE_CLASS_TEMPERATURE], + "OutsideTemp": ["OutsideTemp", TEMP_CELSIUS, None, 4, DEVICE_CLASS_TEMPERATURE], }, "bai": { - "HotWaterTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], - "StorageTemperature": ["StorageTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "HotWaterTemperature": [ + "HwcTemp", + TEMP_CELSIUS, + None, + 4, + DEVICE_CLASS_TEMPERATURE, + ], + "StorageTemperature": [ + "StorageTemp", + TEMP_CELSIUS, + None, + 4, + DEVICE_CLASS_TEMPERATURE, + ], "DesiredStorageTemperature": [ "StorageTempDesired", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, ], "OutdoorsTemperature": [ "OutdoorstempSensor", TEMP_CELSIUS, - "mdi:thermometer", + None, 4, + DEVICE_CLASS_TEMPERATURE, ], - "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4], - "AverageIgnitionTime": ["averageIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], - "MaximumIgnitionTime": ["maxIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], - "MinimumIgnitionTime": ["minIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], - "ReturnTemperature": ["ReturnTemp", TEMP_CELSIUS, "mdi:thermometer", 4], - "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2], - "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2], + "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4, None], + "AverageIgnitionTime": [ + "averageIgnitiontime", + TIME_SECONDS, + "mdi:av-timer", + 0, + None, + ], + "MaximumIgnitionTime": [ + "maxIgnitiontime", + TIME_SECONDS, + "mdi:av-timer", + 0, + None, + ], + "MinimumIgnitionTime": [ + "minIgnitiontime", + TIME_SECONDS, + "mdi:av-timer", + 0, + None, + ], + "ReturnTemperature": [ + "ReturnTemp", + TEMP_CELSIUS, + None, + 4, + DEVICE_CLASS_TEMPERATURE, + ], + "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2, None], + "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2, None], "DesiredFlowTemperature": [ "FlowTempDesired", TEMP_CELSIUS, - "mdi:thermometer", + None, 0, + DEVICE_CLASS_TEMPERATURE, ], - "FlowTemperature": ["FlowTemp", TEMP_CELSIUS, "mdi:thermometer", 4], - "Flame": ["Flame", None, "mdi:toggle-switch", 2], + "FlowTemperature": [ + "FlowTemp", + TEMP_CELSIUS, + None, + 4, + DEVICE_CLASS_TEMPERATURE, + None, + ], + "Flame": ["Flame", None, "mdi:toggle-switch", 2, None], "PowerEnergyConsumptionHeatingCircuit": [ "PrEnergySumHc1", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0, + None, ], "PowerEnergyConsumptionHotWaterCircuit": [ "PrEnergySumHwc1", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0, + None, + ], + "RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2, None], + "HeatingPartLoad": [ + "PartloadHcKW", + ENERGY_KILO_WATT_HOUR, + "mdi:flash", + 0, + None, + ], + "StateNumber": ["StateNumber", None, "mdi:fire", 3, None], + "ModulationPercentage": [ + "ModulationTempDesired", + PERCENTAGE, + "mdi:percent", + 0, + None, ], - "RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2], - "HeatingPartLoad": ["PartloadHcKW", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0], - "StateNumber": ["StateNumber", None, "mdi:fire", 3], - "ModulationPercentage": ["ModulationTempDesired", PERCENTAGE, "mdi:percent", 0], }, } diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 00f6a6b2b3e..abd9620130d 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -41,7 +41,13 @@ class EbusdSensor(SensorEntity): """Initialize the sensor.""" self._state = None self._client_name = name - self._name, self._unit_of_measurement, self._icon, self._type = sensor + ( + self._name, + self._unit_of_measurement, + self._icon, + self._type, + self._device_class, + ) = sensor self.data = data @property @@ -77,6 +83,11 @@ class EbusdSensor(SensorEntity): return schedule return None + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._device_class + @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index e1c9308b5a9..9a2fbdd9b87 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -1,6 +1,6 @@ """Allows reading temperatures from ecoal/esterownik.pl controller.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from . import AVAILABLE_SENSORS, DATA_ECOAL_BOILER @@ -37,6 +37,11 @@ class EcoalTempSensor(SensorEntity): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_TEMPERATURE + @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 28711821f50..9adb7665753 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, + DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, @@ -117,6 +118,11 @@ class EddystoneTemp(SensorEntity): """Return the state of the device.""" return self.temperature + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_TEMPERATURE + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index ae0854ec244..01413ceaec0 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -2,7 +2,12 @@ import logging from homeassistant.components.sensor import SensorEntity -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from . import ( CONF_SENSORS, @@ -172,10 +177,11 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return None @property - def icon(self): - """Icon to use in the frontend, if any.""" + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" if "bed_temp" in self._sensor: - return "mdi:thermometer" + return DEVICE_CLASS_TEMPERATURE + return None async def async_update(self): """Retrieve latest state.""" @@ -334,6 +340,6 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): return TEMP_FAHRENHEIT @property - def icon(self): - """Icon to use in the frontend, if any.""" - return "mdi:thermometer" + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_TEMPERATURE diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 0f0fb04fd00..232bc558da1 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -77,6 +78,7 @@ class ECSensor(SensorEntity): self._state = None self._attr = None self._unit = None + self._device_class = None @property def unique_id(self) -> str: @@ -103,6 +105,11 @@ class ECSensor(SensorEntity): """Return the units of measurement.""" return self._unit + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._device_class + def update(self): """Update current conditions.""" self.ec_data.update() @@ -135,6 +142,7 @@ class ECSensor(SensorEntity): "humidex", ]: self._unit = TEMP_CELSIUS + self._device_class = DEVICE_CLASS_TEMPERATURE else: self._unit = sensor_data.get("unit") diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 137d6aee853..4e10b5e65d1 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, + DEVICE_CLASS_TEMPERATURE, PRESSURE_HPA, TEMP_CELSIUS, VOLT, @@ -24,22 +25,22 @@ CONF_USE_LEDS = "use_leds" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { - "light": ["light", " ", "mdi:weather-sunny"], - "light_red": ["light_red", " ", "mdi:invert-colors"], - "light_green": ["light_green", " ", "mdi:invert-colors"], - "light_blue": ["light_blue", " ", "mdi:invert-colors"], - "accelerometer_x": ["accelerometer_x", "G", "mdi:earth"], - "accelerometer_y": ["accelerometer_y", "G", "mdi:earth"], - "accelerometer_z": ["accelerometer_z", "G", "mdi:earth"], - "magnetometer_x": ["magnetometer_x", " ", "mdi:magnet"], - "magnetometer_y": ["magnetometer_y", " ", "mdi:magnet"], - "magnetometer_z": ["magnetometer_z", " ", "mdi:magnet"], - "temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer"], - "pressure": ["pressure", PRESSURE_HPA, "mdi:gauge"], - "voltage_0": ["voltage_0", VOLT, "mdi:flash"], - "voltage_1": ["voltage_1", VOLT, "mdi:flash"], - "voltage_2": ["voltage_2", VOLT, "mdi:flash"], - "voltage_3": ["voltage_3", VOLT, "mdi:flash"], + "light": ["light", " ", "mdi:weather-sunny", None], + "light_red": ["light_red", " ", "mdi:invert-colors", None], + "light_green": ["light_green", " ", "mdi:invert-colors", None], + "light_blue": ["light_blue", " ", "mdi:invert-colors", None], + "accelerometer_x": ["accelerometer_x", "G", "mdi:earth", None], + "accelerometer_y": ["accelerometer_y", "G", "mdi:earth", None], + "accelerometer_z": ["accelerometer_z", "G", "mdi:earth", None], + "magnetometer_x": ["magnetometer_x", " ", "mdi:magnet", None], + "magnetometer_y": ["magnetometer_y", " ", "mdi:magnet", None], + "magnetometer_z": ["magnetometer_z", " ", "mdi:magnet", None], + "temperature": ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + "pressure": ["pressure", PRESSURE_HPA, "mdi:gauge", None], + "voltage_0": ["voltage_0", VOLT, "mdi:flash", None], + "voltage_1": ["voltage_1", VOLT, "mdi:flash", None], + "voltage_2": ["voltage_2", VOLT, "mdi:flash", None], + "voltage_3": ["voltage_3", VOLT, "mdi:flash", None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -91,6 +92,11 @@ class EnvirophatSensor(SensorEntity): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return SENSOR_TYPES[self.type][3] + @property def icon(self): """Icon to use in the frontend, if any.""" From adb5fd5a038527b412e44a07422589fcf2fc2d5f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 16:47:58 -0400 Subject: [PATCH 236/818] Use entity class attributes for bbox (#52838) * Use entity class attributes for bbox * tweak --- homeassistant/components/bbox/sensor.py | 86 ++++++------------------- 1 file changed, 20 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 5256c2a61a0..b0ace5fa675 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -88,40 +88,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BboxUptimeSensor(SensorEntity): """Bbox uptime sensor.""" + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, bbox_data, sensor_type, name): """Initialize the sensor.""" - self.client_name = name - self.type = sensor_type - self._name = SENSOR_TYPES[sensor_type][0] + self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._icon = SENSOR_TYPES[sensor_type][2] + self._attr_icon = SENSOR_TYPES[sensor_type][2] self.bbox_data = bbox_data - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP def update(self): """Get the latest data from Bbox and update the state.""" @@ -129,60 +104,39 @@ class BboxUptimeSensor(SensorEntity): uptime = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._state = uptime.replace(microsecond=0).isoformat() + self._attr_state = uptime.replace(microsecond=0).isoformat() class BboxSensor(SensorEntity): """Implementation of a Bbox sensor.""" + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + def __init__(self, bbox_data, sensor_type, name): """Initialize the sensor.""" - self.client_name = name self.type = sensor_type - self._name = SENSOR_TYPES[sensor_type][0] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._icon = SENSOR_TYPES[sensor_type][2] + self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_icon = SENSOR_TYPES[sensor_type][2] self.bbox_data = bbox_data - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() if self.type == "down_max_bandwidth": - self._state = round(self.bbox_data.data["rx"]["maxBandwidth"] / 1000, 2) + self._attr_state = round( + self.bbox_data.data["rx"]["maxBandwidth"] / 1000, 2 + ) elif self.type == "up_max_bandwidth": - self._state = round(self.bbox_data.data["tx"]["maxBandwidth"] / 1000, 2) + self._attr_state = round( + self.bbox_data.data["tx"]["maxBandwidth"] / 1000, 2 + ) elif self.type == "current_down_bandwidth": - self._state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) + self._attr_state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) elif self.type == "current_up_bandwidth": - self._state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + self._attr_state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) elif self.type == "number_of_reboots": - self._state = self.bbox_data.router_infos["device"]["numberofboots"] + self._attr_state = self.bbox_data.router_infos["device"]["numberofboots"] class BboxData: From 7ef4bd53ec1c13755ce0e4dbf5efadaa94b03280 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 16:49:38 -0400 Subject: [PATCH 237/818] Use entity class attributes for Blockchain (#52894) * Use entity class attributes for blockchain * rework * tweak --- homeassistant/components/blockchain/sensor.py | 36 ++++--------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 3ecf4bee319..bbb9c892871 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -46,39 +46,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlockchainSensor(SensorEntity): """Representation of a Blockchain.com sensor.""" + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_icon = ICON + _attr_unit_of_measurement = "BTC" + def __init__(self, name, addresses): """Initialize the sensor.""" - self._name = name + self._attr_name = name self.addresses = addresses - self._state = None - self._unit_of_measurement = "BTC" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest state of the sensor.""" - - self._state = get_balance(self.addresses) + self._attr_state = get_balance(self.addresses) From ab5fd70988eecd5f749c7e05db0a61e687ee6460 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 12 Jul 2021 15:50:51 -0500 Subject: [PATCH 238/818] Bump pysonos to 0.0.52 (#52934) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index b1b0bc8a202..e873b43839a 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.51"], + "requirements": ["pysonos==0.0.52"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 2149b72b22e..d2f1af82d2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1771,7 +1771,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.51 +pysonos==0.0.52 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5afe5edbcf..dfa79b8eb08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.51 +pysonos==0.0.52 # homeassistant.components.spc pyspcwebgw==0.4.0 From 9b2107b71f8dc5eac6718c145c64acab342e5a06 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 16:52:38 -0400 Subject: [PATCH 239/818] Use entity class attributes for Blebox (#52890) * Use entity class attributes for blebox * rework * Apply suggestions from code review Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/blebox/__init__.py | 32 ++++++------------- .../components/blebox/air_quality.py | 5 +-- homeassistant/components/blebox/climate.py | 17 ++-------- homeassistant/components/blebox/cover.py | 21 +++++------- homeassistant/components/blebox/light.py | 10 +++--- homeassistant/components/blebox/sensor.py | 16 ++++------ homeassistant/components/blebox/switch.py | 8 ++--- 7 files changed, 37 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 33d09f460db..95b36612add 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -79,16 +79,16 @@ class BleBoxEntity(Entity): def __init__(self, feature): """Initialize a BleBox entity.""" self._feature = feature - - @property - def name(self): - """Return the internal entity name.""" - return self._feature.full_name - - @property - def unique_id(self): - """Return a unique id.""" - return self._feature.unique_id + self._attr_name = feature.full_name + self._attr_unique_id = feature.unique_id + product = feature.product + self._attr_device_info = { + "identifiers": {(DOMAIN, product.unique_id)}, + "name": product.name, + "manufacturer": product.brand, + "model": product.model, + "sw_version": product.firmware_version, + } async def async_update(self): """Update the entity state.""" @@ -96,15 +96,3 @@ class BleBoxEntity(Entity): await self._feature.async_update() except Error as ex: _LOGGER.error("Updating '%s' failed: %s", self.name, ex) - - @property - def device_info(self): - """Return device information for this entity.""" - product = self._feature.product - return { - "identifiers": {(DOMAIN, product.unique_id)}, - "name": product.name, - "manufacturer": product.brand, - "model": product.model, - "sw_version": product.firmware_version, - } diff --git a/homeassistant/components/blebox/air_quality.py b/homeassistant/components/blebox/air_quality.py index e7e9bac1f97..debf0201a3f 100644 --- a/homeassistant/components/blebox/air_quality.py +++ b/homeassistant/components/blebox/air_quality.py @@ -15,10 +15,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BleBoxAirQualityEntity(BleBoxEntity, AirQualityEntity): """Representation of a BleBox air quality feature.""" - @property - def icon(self): - """Return the icon.""" - return "mdi:blur" + _attr_icon = "mdi:blur" @property def particulate_matter_0_1(self): diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 4ee8cf9be76..59e64b772ef 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -25,10 +25,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): """Representation of a BleBox climate feature (saunaBox).""" - @property - def supported_features(self): - """Return the supported climate features.""" - return SUPPORT_TARGET_TEMPERATURE + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT] + _attr_temperature_unit = TEMP_CELSIUS @property def hvac_mode(self): @@ -48,16 +47,6 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): # NOTE: In practice, there's no need to handle case when is_heating is None return CURRENT_HVAC_HEAT if self._feature.is_heating else CURRENT_HVAC_IDLE - @property - def hvac_modes(self): - """Return a list of possible HVAC modes.""" - return [HVAC_MODE_OFF, HVAC_MODE_HEAT] - - @property - def temperature_unit(self): - """Return the temperature unit.""" - return TEMP_CELSIUS - @property def max_temp(self): """Return the maximum temperature supported.""" diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 620adacf3f6..5dc6a486ed3 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -27,24 +27,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BleBoxCoverEntity(BleBoxEntity, CoverEntity): """Representation of a BleBox cover feature.""" + def __init__(self, feature): + """Initialize a BleBox cover feature.""" + super().__init__(feature) + self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + position = SUPPORT_SET_POSITION if feature.is_slider else 0 + stop = SUPPORT_STOP if feature.has_stop else 0 + self._attr_supported_features = position | stop | SUPPORT_OPEN | SUPPORT_CLOSE + @property def state(self): """Return the equivalent HA cover state.""" return BLEBOX_TO_HASS_COVER_STATES[self._feature.state] - @property - def device_class(self): - """Return the device class.""" - return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] - - @property - def supported_features(self): - """Return the supported cover features.""" - position = SUPPORT_SET_POSITION if self._feature.is_slider else 0 - stop = SUPPORT_STOP if self._feature.has_stop else 0 - - return position | stop | SUPPORT_OPEN | SUPPORT_CLOSE - @property def current_cover_position(self): """Return the current cover position.""" diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 9bb7371a97a..b03cc16112c 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -29,13 +29,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BleBoxLightEntity(BleBoxEntity, LightEntity): """Representation of BleBox lights.""" - @property - def supported_color_modes(self): - """Return supported color modes.""" - return {self.color_mode} + def __init__(self, feature): + """Initialize a BleBox light.""" + super().__init__(feature) + self._attr_supported_color_modes = {self.color_mode} @property - def is_on(self): + def is_on(self) -> bool: """Return if light is on.""" return self._feature.is_on diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index c1b9d8501c1..09bfca88776 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -17,17 +17,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BleBoxSensorEntity(BleBoxEntity, SensorEntity): """Representation of a BleBox sensor feature.""" + def __init__(self, feature): + """Initialize a BleBox sensor feature.""" + super().__init__(feature) + self._attr_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] + self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + @property def state(self): """Return the state.""" return self._feature.current - - @property - def unit_of_measurement(self): - """Return the unit.""" - return BLEBOX_TO_UNIT_MAP[self._feature.unit] - - @property - def device_class(self): - """Return the device class.""" - return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index e88773db639..3769235e943 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -15,10 +15,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): """Representation of a BleBox switch feature.""" - @property - def device_class(self): - """Return the device class.""" - return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + def __init__(self, feature): + """Initialize a BleBox switch feature.""" + super().__init__(feature) + self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] @property def is_on(self): From 4d16cda95773c4d154c61c6f6bfaab1ad1fc8436 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 12 Jul 2021 22:56:10 +0200 Subject: [PATCH 240/818] ESPHome enable static type checking (#52348) --- .strict-typing | 1 + homeassistant/components/esphome/__init__.py | 261 +++++++++++------- .../components/esphome/binary_sensor.py | 25 +- homeassistant/components/esphome/camera.py | 25 +- homeassistant/components/esphome/climate.py | 55 ++-- .../components/esphome/config_flow.py | 53 ++-- homeassistant/components/esphome/cover.py | 41 ++- .../components/esphome/entry_data.py | 25 +- homeassistant/components/esphome/fan.py | 38 ++- homeassistant/components/esphome/light.py | 39 ++- homeassistant/components/esphome/number.py | 18 +- homeassistant/components/esphome/sensor.py | 37 +-- homeassistant/components/esphome/switch.py | 28 +- mypy.ini | 14 +- script/hassfest/mypy_config.py | 1 - tests/components/esphome/test_config_flow.py | 7 + 16 files changed, 364 insertions(+), 304 deletions(-) diff --git a/.strict-typing b/.strict-typing index 77d00928edd..1a12b1c5299 100644 --- a/.strict-typing +++ b/.strict-typing @@ -31,6 +31,7 @@ homeassistant.components.dnsip.* homeassistant.components.dsmr.* homeassistant.components.dunehd.* homeassistant.components.elgato.* +homeassistant.components.esphome.* homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* homeassistant.components.forecast_solar.* diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index aa7da100505..0db72ad8f3b 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -2,15 +2,17 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable from dataclasses import dataclass, field import functools import logging import math -from typing import Generic, TypeVar +from typing import Any, Callable, Generic, TypeVar, cast, overload from aioesphomeapi import ( APIClient, APIConnectionError, + APIIntEnum, APIVersion, DeviceInfo as EsphomeDeviceInfo, EntityInfo, @@ -32,13 +34,14 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_set_service_schema @@ -97,7 +100,7 @@ class DomainData: """Get the global DomainData instance stored in hass.data.""" # Don't use setdefault - this is a hot code path if DOMAIN in hass.data: - return hass.data[DOMAIN] + return cast(_T, hass.data[DOMAIN]) ret = hass.data[DOMAIN] = cls() return ret @@ -153,7 +156,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if service.data_template: try: data_template = { - key: Template(value) for key, value in service.data_template.items() + key: Template(value) # type: ignore[no-untyped-call] + for key, value in service.data_template.items() } template.attach(hass, data_template) service_data.update( @@ -197,10 +201,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: send_state = state.state if attribute: - send_state = state.attributes[attribute] + attr_val = state.attributes[attribute] # ESPHome only handles "on"/"off" for boolean values - if isinstance(send_state, bool): - send_state = "on" if send_state else "off" + if isinstance(attr_val, bool): + send_state = "on" if attr_val else "off" + else: + send_state = attr_val await cli.send_home_assistant_state(entity_id, attribute, str(send_state)) @@ -253,6 +259,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nonlocal device_id try: entry_data.device_info = await cli.device_info() + assert cli.api_version is not None entry_data.api_version = cli.api_version entry_data.available = True device_id = await _async_setup_device_registry( @@ -304,9 +311,9 @@ class ReconnectLogic(RecordUpdateListener): cli: APIClient, entry: ConfigEntry, host: str, - on_login, + on_login: Callable[[], Awaitable[None]], zc: Zeroconf, - ): + ) -> None: """Initialize ReconnectingLogic.""" self._hass = hass self._cli = cli @@ -322,12 +329,12 @@ class ReconnectLogic(RecordUpdateListener): # Event the different strategies use for issuing a reconnect attempt. self._reconnect_event = asyncio.Event() # The task containing the infinite reconnect loop while running - self._loop_task: asyncio.Task | None = None + self._loop_task: asyncio.Task[None] | None = None # How many reconnect attempts have there been already, used for exponential wait time self._tries = 0 self._tries_lock = asyncio.Lock() # Track the wait task to cancel it on HA shutdown - self._wait_task: asyncio.Task | None = None + self._wait_task: asyncio.Task[None] | None = None self._wait_task_lock = asyncio.Lock() @property @@ -338,7 +345,7 @@ class ReconnectLogic(RecordUpdateListener): except KeyError: return None - async def _on_disconnect(self): + async def _on_disconnect(self) -> None: """Log and issue callbacks when disconnecting.""" if self._entry_data is None: return @@ -364,7 +371,7 @@ class ReconnectLogic(RecordUpdateListener): self._connected = False self._reconnect_event.set() - async def _wait_and_start_reconnect(self): + async def _wait_and_start_reconnect(self) -> None: """Wait for exponentially increasing time to issue next reconnect event.""" async with self._tries_lock: tries = self._tries @@ -383,7 +390,7 @@ class ReconnectLogic(RecordUpdateListener): self._wait_task = None self._reconnect_event.set() - async def _try_connect(self): + async def _try_connect(self) -> None: """Try connecting to the API client.""" async with self._tries_lock: tries = self._tries @@ -421,7 +428,7 @@ class ReconnectLogic(RecordUpdateListener): await self._stop_zc_listen() self._hass.async_create_task(self._on_login()) - async def _reconnect_once(self): + async def _reconnect_once(self) -> None: # Wait and clear reconnection event await self._reconnect_event.wait() self._reconnect_event.clear() @@ -429,7 +436,7 @@ class ReconnectLogic(RecordUpdateListener): # If in connected state, do not try to connect again. async with self._connected_lock: if self._connected: - return False + return # Check if the entry got removed or disabled, in which case we shouldn't reconnect if not DomainData.get(self._hass).is_entry_loaded(self._entry): @@ -448,7 +455,7 @@ class ReconnectLogic(RecordUpdateListener): await self._try_connect() - async def _reconnect_loop(self): + async def _reconnect_loop(self) -> None: while True: try: await self._reconnect_once() @@ -457,7 +464,7 @@ class ReconnectLogic(RecordUpdateListener): except Exception: # pylint: disable=broad-except _LOGGER.error("Caught exception while reconnecting", exc_info=True) - async def start(self): + async def start(self) -> None: """Start the reconnecting logic background task.""" # Create reconnection loop outside of HA's tracked tasks in order # not to delay startup. @@ -467,7 +474,7 @@ class ReconnectLogic(RecordUpdateListener): self._connected = False self._reconnect_event.set() - async def stop(self): + async def stop(self) -> None: """Stop the reconnecting logic background task. Does not disconnect the client.""" if self._loop_task is not None: self._loop_task.cancel() @@ -478,7 +485,7 @@ class ReconnectLogic(RecordUpdateListener): self._wait_task = None await self._stop_zc_listen() - async def _start_zc_listen(self): + async def _start_zc_listen(self) -> None: """Listen for mDNS records. This listener allows us to schedule a reconnect as soon as a @@ -491,7 +498,7 @@ class ReconnectLogic(RecordUpdateListener): ) self._zc_listening = True - async def _stop_zc_listen(self): + async def _stop_zc_listen(self) -> None: """Stop listening for zeroconf updates.""" async with self._zc_lock: if self._zc_listening: @@ -499,12 +506,12 @@ class ReconnectLogic(RecordUpdateListener): self._zc_listening = False @callback - def stop_callback(self): + def stop_callback(self) -> None: """Stop as an async callback function.""" self._hass.async_create_task(self.stop()) @callback - def _set_reconnect(self): + def _set_reconnect(self) -> None: self._reconnect_event.set() def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: @@ -535,13 +542,13 @@ class ReconnectLogic(RecordUpdateListener): async def _async_setup_device_registry( hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo -): +) -> str: """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" device_registry = await dr.async_get_registry(hass) - entry = device_registry.async_get_or_create( + device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, @@ -549,63 +556,76 @@ async def _async_setup_device_registry( model=device_info.model, sw_version=sw_version, ) - return entry.id + return device_entry.id + + +ARG_TYPE_METADATA = { + UserServiceArgType.BOOL: { + "validator": cv.boolean, + "example": "False", + "selector": {"boolean": None}, + }, + UserServiceArgType.INT: { + "validator": vol.Coerce(int), + "example": "42", + "selector": {"number": {CONF_MODE: "box"}}, + }, + UserServiceArgType.FLOAT: { + "validator": vol.Coerce(float), + "example": "12.3", + "selector": {"number": {CONF_MODE: "box", "step": 1e-3}}, + }, + UserServiceArgType.STRING: { + "validator": cv.string, + "example": "Example text", + "selector": {"text": None}, + }, + UserServiceArgType.BOOL_ARRAY: { + "validator": [cv.boolean], + "description": "A list of boolean values.", + "example": "[True, False]", + "selector": {"object": {}}, + }, + UserServiceArgType.INT_ARRAY: { + "validator": [vol.Coerce(int)], + "description": "A list of integer values.", + "example": "[42, 34]", + "selector": {"object": {}}, + }, + UserServiceArgType.FLOAT_ARRAY: { + "validator": [vol.Coerce(float)], + "description": "A list of floating point numbers.", + "example": "[ 12.3, 34.5 ]", + "selector": {"object": {}}, + }, + UserServiceArgType.STRING_ARRAY: { + "validator": [cv.string], + "description": "A list of strings.", + "example": "['Example text', 'Another example']", + "selector": {"object": {}}, + }, +} async def _register_service( hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService -): +) -> None: + if entry_data.device_info is None: + raise ValueError("Device Info needs to be fetched first") service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" schema = {} fields = {} for arg in service.args: - metadata = { - UserServiceArgType.BOOL: { - "validator": cv.boolean, - "example": "False", - "selector": {"boolean": None}, - }, - UserServiceArgType.INT: { - "validator": vol.Coerce(int), - "example": "42", - "selector": {"number": {CONF_MODE: "box"}}, - }, - UserServiceArgType.FLOAT: { - "validator": vol.Coerce(float), - "example": "12.3", - "selector": {"number": {CONF_MODE: "box", "step": 1e-3}}, - }, - UserServiceArgType.STRING: { - "validator": cv.string, - "example": "Example text", - "selector": {"text": None}, - }, - UserServiceArgType.BOOL_ARRAY: { - "validator": [cv.boolean], - "description": "A list of boolean values.", - "example": "[True, False]", - "selector": {"object": {}}, - }, - UserServiceArgType.INT_ARRAY: { - "validator": [vol.Coerce(int)], - "description": "A list of integer values.", - "example": "[42, 34]", - "selector": {"object": {}}, - }, - UserServiceArgType.FLOAT_ARRAY: { - "validator": [vol.Coerce(float)], - "description": "A list of floating point numbers.", - "example": "[ 12.3, 34.5 ]", - "selector": {"object": {}}, - }, - UserServiceArgType.STRING_ARRAY: { - "validator": [cv.string], - "description": "A list of strings.", - "example": "['Example text', 'Another example']", - "selector": {"object": {}}, - }, - }[arg.type] + if arg.type not in ARG_TYPE_METADATA: + _LOGGER.error( + "Can't register service %s because %s is of unknown type %s", + service_name, + arg.name, + arg.type, + ) + return + metadata = ARG_TYPE_METADATA[arg.type] schema[vol.Required(arg.name)] = metadata["validator"] fields[arg.name] = { "name": arg.name, @@ -615,8 +635,8 @@ async def _register_service( "selector": metadata["selector"], } - async def execute_service(call): - await entry_data.client.execute_service(service, call.data) + async def execute_service(call: ServiceCall) -> None: + await entry_data.client.execute_service(service, call.data) # type: ignore[arg-type] hass.services.async_register( DOMAIN, service_name, execute_service, vol.Schema(schema) @@ -632,7 +652,7 @@ async def _register_service( async def _setup_services( hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService] -): +) -> None: old_services = entry_data.services.copy() to_unregister = [] to_register = [] @@ -653,6 +673,7 @@ async def _setup_services( entry_data.services = {serv.key: serv for serv in services} + assert entry_data.device_info is not None for service in to_unregister: service_name = f"{entry_data.device_info.name}_{service.name}" hass.services.async_remove(DOMAIN, service_name) @@ -688,15 +709,20 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await DomainData.get(hass).get_or_create_store(hass, entry).async_remove() +_InfoT = TypeVar("_InfoT", bound=EntityInfo) +_EntityT = TypeVar("_EntityT", bound="EsphomeBaseEntity[Any,Any]") +_StateT = TypeVar("_StateT", bound=EntityState) + + async def platform_async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities, + async_add_entities: AddEntitiesCallback, *, component_key: str, - info_type, - entity_type, - state_type, + info_type: type[_InfoT], + entity_type: type[_EntityT], + state_type: type[_StateT], ) -> None: """Set up an esphome platform. @@ -709,15 +735,17 @@ async def platform_async_setup_entry( entry_data.state[component_key] = {} @callback - def async_list_entities(infos: list[EntityInfo]): + def async_list_entities(infos: list[EntityInfo]) -> None: """Update entities of this platform when entities are listed.""" old_infos = entry_data.info[component_key] - new_infos = {} + new_infos: dict[int, EntityInfo] = {} add_entities = [] for info in infos: if not isinstance(info, info_type): # Filter out infos that don't belong to this platform. continue + # cast back to upper type, otherwise mypy gets confused + info = cast(EntityInfo, info) if info.key in old_infos: # Update existing entity @@ -746,10 +774,13 @@ async def platform_async_setup_entry( ) @callback - def async_entity_state(state: EntityState): + def async_entity_state(state: EntityState) -> None: """Notify the appropriate entity of an updated state.""" if not isinstance(state, state_type): return + # cast back to upper type, otherwise mypy gets confused + state = cast(EntityState, state) + entry_data.state[component_key][state.key] = state entry_data.async_update_entity(hass, component_key, state.key) @@ -759,16 +790,20 @@ async def platform_async_setup_entry( ) -def esphome_state_property(func): +_PropT = TypeVar("_PropT", bound=Callable[..., Any]) + + +def esphome_state_property(func: _PropT) -> _PropT: """Wrap a state property of an esphome entity. This checks if the state object in the entity is set, and prevents writing NAN values to the Home Assistant state machine. """ - @property - def _wrapper(self): - if self._state is None: + @property # type: ignore[misc] + @functools.wraps(func) + def _wrapper(self): # type: ignore[no-untyped-def] + if not self._has_state: return None val = func(self) if isinstance(val, float) and math.isnan(val): @@ -777,29 +812,43 @@ def esphome_state_property(func): return None return val - return _wrapper + return cast(_PropT, _wrapper) -class EsphomeEnumMapper(Generic[_T]): +_EnumT = TypeVar("_EnumT", bound=APIIntEnum) +_ValT = TypeVar("_ValT") + + +class EsphomeEnumMapper(Generic[_EnumT, _ValT]): """Helper class to convert between hass and esphome enum values.""" - def __init__(self, mapping: dict[_T, str]) -> None: + def __init__(self, mapping: dict[_EnumT, _ValT]) -> None: """Construct a EsphomeEnumMapper.""" # Add none mapping - mapping = {None: None, **mapping} - self._mapping = mapping - self._inverse: dict[str, _T] = {v: k for k, v in mapping.items()} + augmented_mapping: dict[_EnumT | None, _ValT | None] = mapping # type: ignore[assignment] + augmented_mapping[None] = None - def from_esphome(self, value: _T | None) -> str | None: + self._mapping = augmented_mapping + self._inverse: dict[_ValT, _EnumT] = {v: k for k, v in mapping.items()} + + @overload + def from_esphome(self, value: _EnumT) -> _ValT: + ... + + @overload + def from_esphome(self, value: _EnumT | None) -> _ValT | None: + ... + + def from_esphome(self, value: _EnumT | None) -> _ValT | None: """Convert from an esphome int representation to a hass string.""" return self._mapping[value] - def from_hass(self, value: str) -> _T: + def from_hass(self, value: _ValT) -> _EnumT: """Convert from a hass string to a esphome int representation.""" return self._inverse[value] -class EsphomeBaseEntity(Entity): +class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" def __init__( @@ -850,17 +899,18 @@ class EsphomeBaseEntity(Entity): return self._entry_data.api_version @property - def _static_info(self) -> EntityInfo: + def _static_info(self) -> _InfoT: # Check if value is in info database. Use a single lookup. info = self._entry_data.info[self._component_key].get(self._key) if info is not None: - return info + return cast(_InfoT, info) # This entity is in the removal project and has been removed from .info # already, look in old_info - return self._entry_data.old_info[self._component_key].get(self._key) + return cast(_InfoT, self._entry_data.old_info[self._component_key][self._key]) @property def _device_info(self) -> EsphomeDeviceInfo: + assert self._entry_data.device_info is not None return self._entry_data.device_info @property @@ -868,11 +918,12 @@ class EsphomeBaseEntity(Entity): return self._entry_data.client @property - def _state(self) -> EntityState | None: - try: - return self._entry_data.state[self._component_key][self._key] - except KeyError: - return None + def _state(self) -> _StateT: + return cast(_StateT, self._entry_data.state[self._component_key][self._key]) + + @property + def _has_state(self) -> bool: + return self._key in self._entry_data.state[self._component_key] @property def available(self) -> bool: @@ -911,7 +962,7 @@ class EsphomeBaseEntity(Entity): return False -class EsphomeEntity(EsphomeBaseEntity): +class EsphomeEntity(EsphomeBaseEntity[_InfoT, _StateT]): """Define a generic esphome entity.""" async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 28cc47691f5..44ed1806ed6 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -4,11 +4,16 @@ from __future__ import annotations from aioesphomeapi import BinarySensorInfo, BinarySensorState from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EsphomeEntity, platform_async_setup_entry -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up ESPHome binary sensors based on a config entry.""" await platform_async_setup_entry( hass, @@ -21,17 +26,15 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class EsphomeBinarySensor(EsphomeEntity, BinarySensorEntity): +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=no-member + + +class EsphomeBinarySensor( + EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity +): """A binary sensor implementation for ESPHome.""" - @property - def _static_info(self) -> BinarySensorInfo: - return super()._static_info - - @property - def _state(self) -> BinarySensorState | None: - return super()._state - @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" @@ -39,7 +42,7 @@ class EsphomeBinarySensor(EsphomeEntity, BinarySensorEntity): # Status binary sensors indicated connected state. # So in their case what's usually _availability_ is now state return self._entry_data.available - if self._state is None: + if not self._has_state: return None if self._state.missing_state: return None diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index f047d5c1bdd..34b6d90f4d4 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -2,20 +2,23 @@ from __future__ import annotations import asyncio +from typing import Any from aioesphomeapi import CameraInfo, CameraState +from aiohttp import web from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EsphomeBaseEntity, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up esphome cameras based on a config entry.""" await platform_async_setup_entry( @@ -29,23 +32,19 @@ async def async_setup_entry( ) -class EsphomeCamera(Camera, EsphomeBaseEntity): +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=no-member + + +class EsphomeCamera(Camera, EsphomeBaseEntity[CameraInfo, CameraState]): """A camera implementation for ESPHome.""" - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize.""" Camera.__init__(self) EsphomeBaseEntity.__init__(self, *args, **kwargs) self._image_cond = asyncio.Condition() - @property - def _static_info(self) -> CameraInfo: - return super()._static_info - - @property - def _state(self) -> CameraState | None: - return super()._state - async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -90,7 +89,9 @@ class EsphomeCamera(Camera, EsphomeBaseEntity): return None return self._state.data[:] - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse: """Serve an HTTP MJPEG stream from the camera.""" return await camera.async_get_still_stream( request, self._async_camera_stream_image, camera.DEFAULT_CONTENT_TYPE, 0.0 diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index f7ebccc8434..8715cb368c2 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -1,6 +1,8 @@ """Support for ESPHome climate devices.""" from __future__ import annotations +from typing import Any, cast + from aioesphomeapi import ( ClimateAction, ClimateFanMode, @@ -56,6 +58,7 @@ from homeassistant.components.climate.const import ( SWING_OFF, SWING_VERTICAL, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_HALVES, @@ -63,6 +66,8 @@ from homeassistant.const import ( PRECISION_WHOLE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( EsphomeEntity, @@ -72,7 +77,9 @@ from . import ( ) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up ESPHome climate devices based on a config entry.""" await platform_async_setup_entry( hass, @@ -85,7 +92,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper( +_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode, str] = EsphomeEnumMapper( { ClimateMode.OFF: HVAC_MODE_OFF, ClimateMode.HEAT_COOL: HVAC_MODE_HEAT_COOL, @@ -96,7 +103,7 @@ _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper( ClimateMode.AUTO: HVAC_MODE_AUTO, } ) -_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper( +_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction, str] = EsphomeEnumMapper( { ClimateAction.OFF: CURRENT_HVAC_OFF, ClimateAction.COOLING: CURRENT_HVAC_COOL, @@ -106,7 +113,7 @@ _CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper( ClimateAction.FAN: CURRENT_HVAC_FAN, } ) -_FAN_MODES: EsphomeEnumMapper[ClimateFanMode] = EsphomeEnumMapper( +_FAN_MODES: EsphomeEnumMapper[ClimateFanMode, str] = EsphomeEnumMapper( { ClimateFanMode.ON: FAN_ON, ClimateFanMode.OFF: FAN_OFF, @@ -119,7 +126,7 @@ _FAN_MODES: EsphomeEnumMapper[ClimateFanMode] = EsphomeEnumMapper( ClimateFanMode.DIFFUSE: FAN_DIFFUSE, } ) -_SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper( +_SWING_MODES: EsphomeEnumMapper[ClimateSwingMode, str] = EsphomeEnumMapper( { ClimateSwingMode.OFF: SWING_OFF, ClimateSwingMode.BOTH: SWING_BOTH, @@ -127,7 +134,7 @@ _SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper( ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } ) -_PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper( +_PRESETS: EsphomeEnumMapper[ClimatePreset, str] = EsphomeEnumMapper( { ClimatePreset.NONE: PRESET_NONE, ClimatePreset.HOME: PRESET_HOME, @@ -141,17 +148,14 @@ _PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper( ) -class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member + + +class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity): """A climate implementation for ESPHome.""" - @property - def _static_info(self) -> ClimateInfo: - return super()._static_info - - @property - def _state(self) -> ClimateState | None: - return super()._state - @property def precision(self) -> float: """Return the precision of the climate device.""" @@ -192,7 +196,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): ] + self._static_info.supported_custom_presets @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """Return the list of available swing modes.""" return [ _SWING_MODES.from_esphome(mode) @@ -231,11 +235,8 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): features |= SUPPORT_SWING_MODE return features - # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property - # pylint: disable=invalid-overridden-method - @esphome_state_property - def hvac_mode(self) -> str | None: + def hvac_mode(self) -> str | None: # type: ignore[override] """Return current operation ie. heat, cool, idle.""" return _CLIMATE_MODES.from_esphome(self._state.mode) @@ -286,11 +287,11 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): """Return the highbound target temperature we try to reach.""" return self._state.target_temperature_high - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: float | str) -> None: """Set new target temperature (and operation mode if set).""" - data = {"key": self._static_info.key} + data: dict[str, Any] = {"key": self._static_info.key} if ATTR_HVAC_MODE in kwargs: - data["mode"] = _CLIMATE_MODES.from_hass(kwargs[ATTR_HVAC_MODE]) + data["mode"] = _CLIMATE_MODES.from_hass(cast(str, kwargs[ATTR_HVAC_MODE])) if ATTR_TEMPERATURE in kwargs: data["target_temperature"] = kwargs[ATTR_TEMPERATURE] if ATTR_TARGET_TEMP_LOW in kwargs: @@ -307,21 +308,21 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - kwargs = {} + kwargs: dict[str, Any] = {"key": self._static_info.key} if preset_mode in self._static_info.supported_custom_presets: kwargs["custom_preset"] = preset_mode else: kwargs["preset"] = _PRESETS.from_hass(preset_mode) - await self._client.climate_command(key=self._static_info.key, **kwargs) + await self._client.climate_command(**kwargs) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" - kwargs = {} + kwargs: dict[str, Any] = {"key": self._static_info.key} if fan_mode in self._static_info.supported_custom_fan_modes: kwargs["custom_fan_mode"] = fan_mode else: kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode) - await self._client.climate_command(key=self._static_info.key, **kwargs) + await self._client.climate_command(**kwargs) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 38e44b12508..3062b9690bf 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -2,14 +2,16 @@ from __future__ import annotations from collections import OrderedDict +from typing import Any -from aioesphomeapi import APIClient, APIConnectionError +from aioesphomeapi import APIClient, APIConnectionError, DeviceInfo import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, DomainData @@ -20,20 +22,19 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize flow.""" self._host: str | None = None self._port: int | None = None self._password: str | None = None - async def async_step_user( + async def _async_step_user_base( self, user_input: ConfigType | None = None, error: str | None = None - ): # pylint: disable=arguments-differ - """Handle a flow initialized by the user.""" + ) -> FlowResult: if user_input is not None: return await self._async_authenticate_or_add(user_input) - fields = OrderedDict() + fields: dict[Any, type] = OrderedDict() fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str fields[vol.Optional(CONF_PORT, default=self._port or 6053)] = int @@ -45,26 +46,33 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema(fields), errors=errors ) + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + """Handle a flow initialized by the user.""" + return await self._async_step_user_base(user_input=user_input) + @property - def _name(self): + def _name(self) -> str | None: return self.context.get(CONF_NAME) @_name.setter - def _name(self, value): + def _name(self, value: str) -> None: self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} - def _set_user_input(self, user_input): + def _set_user_input(self, user_input: ConfigType | None) -> None: if user_input is None: return self._host = user_input[CONF_HOST] self._port = user_input[CONF_PORT] - async def _async_authenticate_or_add(self, user_input): + async def _async_authenticate_or_add( + self, user_input: ConfigType | None + ) -> FlowResult: self._set_user_input(user_input) error, device_info = await self.fetch_device_info() if error is not None: - return await self.async_step_user(error=error) + return await self._async_step_user_base(error=error) + assert device_info is not None self._name = device_info.name # Only show authentication step if device uses password @@ -73,7 +81,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_get_entry() - async def async_step_discovery_confirm(self, user_input=None): + async def async_step_discovery_confirm( + self, user_input: ConfigType | None = None + ) -> FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: return await self._async_authenticate_or_add(None) @@ -81,7 +91,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): step_id="discovery_confirm", description_placeholders={"name": self._name} ) - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. local_name = discovery_info["hostname"][:-1] @@ -129,7 +141,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() @callback - def _async_get_entry(self): + def _async_get_entry(self) -> FlowResult: + assert self._name is not None return self.async_create_entry( title=self._name, data={ @@ -140,7 +153,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_authenticate(self, user_input=None, error=None): + async def async_step_authenticate( + self, user_input: ConfigType | None = None, error: str | None = None + ) -> FlowResult: """Handle getting password for authentication.""" if user_input is not None: self._password = user_input[CONF_PASSWORD] @@ -160,9 +175,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def fetch_device_info(self): + async def fetch_device_info(self) -> tuple[str | None, DeviceInfo | None]: """Fetch device info from API and return any errors.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) + assert self._host is not None + assert self._port is not None cli = APIClient( self.hass.loop, self._host, @@ -183,9 +200,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return None, device_info - async def try_login(self): + async def try_login(self) -> str | None: """Try logging in to device and return any errors.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) + assert self._host is not None + assert self._port is not None cli = APIClient( self.hass.loop, self._host, diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 3064f827d7f..d0d89cf40ad 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -1,6 +1,8 @@ """Support for ESPHome covers.""" from __future__ import annotations +from typing import Any + from aioesphomeapi import CoverInfo, CoverOperation, CoverState from homeassistant.components.cover import ( @@ -17,12 +19,13 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ESPHome covers based on a config entry.""" await platform_async_setup_entry( @@ -36,12 +39,13 @@ async def async_setup_entry( ) -class EsphomeCover(EsphomeEntity, CoverEntity): - """A cover implementation for ESPHome.""" +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member - @property - def _static_info(self) -> CoverInfo: - return super()._static_info + +class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): + """A cover implementation for ESPHome.""" @property def supported_features(self) -> int: @@ -63,13 +67,6 @@ class EsphomeCover(EsphomeEntity, CoverEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state - @property - def _state(self) -> CoverState | None: - return super()._state - - # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property - # pylint: disable=invalid-overridden-method - @esphome_state_property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" @@ -94,39 +91,39 @@ class EsphomeCover(EsphomeEntity, CoverEntity): return round(self._state.position * 100.0) @esphome_state_property - def current_cover_tilt_position(self) -> float | None: + def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. 0 is closed, 100 is open.""" if not self._static_info.supports_tilt: return None - return self._state.tilt * 100.0 + return round(self._state.tilt * 100.0) - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._client.cover_command(key=self._static_info.key, position=1.0) - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self._client.cover_command(key=self._static_info.key, position=0.0) - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._client.cover_command(key=self._static_info.key, stop=True) - async def async_set_cover_position(self, **kwargs) -> None: + async def async_set_cover_position(self, **kwargs: int) -> None: """Move the cover to a specific position.""" await self._client.cover_command( key=self._static_info.key, position=kwargs[ATTR_POSITION] / 100 ) - async def async_open_cover_tilt(self, **kwargs) -> None: + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" await self._client.cover_command(key=self._static_info.key, tilt=1.0) - async def async_close_cover_tilt(self, **kwargs) -> None: + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" await self._client.cover_command(key=self._static_info.key, tilt=0.0) - async def async_set_cover_tilt_position(self, **kwargs) -> None: + async def async_set_cover_tilt_position(self, **kwargs: int) -> None: """Move the cover tilt to a specific position.""" await self._client.cover_command( key=self._static_info.key, tilt=kwargs[ATTR_TILT_POSITION] / 100 diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index f60d7cfefb5..3ab933f75f9 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -3,10 +3,11 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Callable +from typing import Any, Callable, cast from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, + APIClient, APIVersion, BinarySensorInfo, CameraInfo, @@ -29,13 +30,10 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store -if TYPE_CHECKING: - from . import APIClient - SAVE_DELAY = 120 # Mapping from ESPHome info type to HA platform -INFO_TYPE_TO_PLATFORM = { +INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { BinarySensorInfo: "binary_sensor", CameraInfo: "camera", ClimateInfo: "climate", @@ -56,14 +54,14 @@ class RuntimeEntryData: entry_id: str client: APIClient store: Store - state: dict[str, dict[str, Any]] = field(default_factory=dict) - info: dict[str, dict[str, Any]] = field(default_factory=dict) + state: dict[str, dict[int, EntityState]] = field(default_factory=dict) + info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires # some static info to be accessible during removal (unique_id, maybe others) # If an entity can't find anything in the info array, it will look for info here. - old_info: dict[str, dict[str, Any]] = field(default_factory=dict) + old_info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) services: dict[int, UserService] = field(default_factory=dict) available: bool = False @@ -73,7 +71,7 @@ class RuntimeEntryData: disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) - _storage_contents: dict | None = None + _storage_contents: dict[str, Any] | None = None @callback def async_update_entity( @@ -93,7 +91,7 @@ class RuntimeEntryData: async def _ensure_platforms_loaded( self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[str] - ): + ) -> None: async with self.platform_load_lock: needed = platforms - self.loaded_platforms tasks = [] @@ -139,6 +137,7 @@ class RuntimeEntryData: restored = await self.store.async_load() if restored is None: return [], [] + restored = cast("dict[str, Any]", restored) self._storage_contents = restored.copy() self.device_info = DeviceInfo.from_dict(restored.pop("device_info")) @@ -157,7 +156,9 @@ class RuntimeEntryData: async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" - store_data = { + if self.device_info is None: + raise ValueError("device_info is not set yet") + store_data: dict[str, Any] = { "device_info": self.device_info.to_dict(), "services": [], "api_version": self.api_version.to_dict(), @@ -171,7 +172,7 @@ class RuntimeEntryData: if store_data == self._storage_contents: return - def _memorized_storage(): + def _memorized_storage() -> dict[str, Any]: self._storage_contents = store_data return store_data diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index e02958d5885..7052ee42861 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from aioesphomeapi import FanDirection, FanInfo, FanSpeed, FanState @@ -15,6 +16,7 @@ from homeassistant.components.fan import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, @@ -33,7 +35,7 @@ ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ESPHome fans based on a config entry.""" await platform_async_setup_entry( @@ -47,7 +49,7 @@ async def async_setup_entry( ) -_FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection] = EsphomeEnumMapper( +_FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection, str] = EsphomeEnumMapper( { FanDirection.FORWARD: DIRECTION_FORWARD, FanDirection.REVERSE: DIRECTION_REVERSE, @@ -55,29 +57,26 @@ _FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection] = EsphomeEnumMapper( ) -class EsphomeFan(EsphomeEntity, FanEntity): +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member + + +class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): """A fan implementation for ESPHome.""" - @property - def _static_info(self) -> FanInfo: - return super()._static_info - - @property - def _state(self) -> FanState | None: - return super()._state - @property def _supports_speed_levels(self) -> bool: api_version = self._api_version return api_version.major == 1 and api_version.minor > 3 - async def async_set_percentage(self, percentage: int) -> None: + async def async_set_percentage(self, percentage: int | None) -> None: """Set the speed percentage of the fan.""" if percentage == 0: await self.async_turn_off() return - data = {"key": self._static_info.key, "state": True} + data: dict[str, Any] = {"key": self._static_info.key, "state": True} if percentage is not None: if self._supports_speed_levels: data["speed_level"] = math.ceil( @@ -97,12 +96,12 @@ class EsphomeFan(EsphomeEntity, FanEntity): speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn on the fan.""" await self.async_set_percentage(percentage) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" await self._client.fan_command(key=self._static_info.key, state=False) @@ -112,17 +111,14 @@ class EsphomeFan(EsphomeEntity, FanEntity): key=self._static_info.key, oscillating=oscillating ) - async def async_set_direction(self, direction: str): + async def async_set_direction(self, direction: str) -> None: """Set direction of the fan.""" await self._client.fan_command( key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction) ) - # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property - # pylint: disable=invalid-overridden-method - @esphome_state_property - def is_on(self) -> bool | None: + def is_on(self) -> bool | None: # type: ignore[override] """Return true if the entity is on.""" return self._state.state @@ -134,7 +130,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): if not self._supports_speed_levels: return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, self._state.speed + ORDERED_NAMED_FAN_SPEEDS, self._state.speed # type: ignore[misc] ) return ranged_value_to_percentage( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index d1f567c3c8e..0dd024832bb 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -1,6 +1,8 @@ """Support for ESPHome lights.""" from __future__ import annotations +from typing import Any + from aioesphomeapi import LightInfo, LightState from homeassistant.components.light import ( @@ -24,6 +26,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry @@ -32,7 +35,7 @@ FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ESPHome lights based on a config entry.""" await platform_async_setup_entry( @@ -46,28 +49,22 @@ async def async_setup_entry( ) -class EsphomeLight(EsphomeEntity, LightEntity): - """A switch implementation for ESPHome.""" +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member - @property - def _static_info(self) -> LightInfo: - return super()._static_info - @property - def _state(self) -> LightState | None: - return super()._state - - # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property - # pylint: disable=invalid-overridden-method +class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): + """A light implementation for ESPHome.""" @esphome_state_property - def is_on(self) -> bool | None: - """Return true if the switch is on.""" + def is_on(self) -> bool | None: # type: ignore[override] + """Return true if the light is on.""" return self._state.state - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - data = {"key": self._static_info.key, "state": True} + data: dict[str, Any] = {"key": self._static_info.key, "state": True} if ATTR_HS_COLOR in kwargs: hue, sat = kwargs[ATTR_HS_COLOR] red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) @@ -86,9 +83,9 @@ class EsphomeLight(EsphomeEntity, LightEntity): data["white"] = kwargs[ATTR_WHITE_VALUE] / 255 await self._client.light_command(**data) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - data = {"key": self._static_info.key, "state": False} + data: dict[str, Any] = {"key": self._static_info.key, "state": False} if ATTR_FLASH in kwargs: data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: @@ -108,7 +105,7 @@ class EsphomeLight(EsphomeEntity, LightEntity): ) @esphome_state_property - def color_temp(self) -> float | None: + def color_temp(self) -> float | None: # type: ignore[override] """Return the CT color value in mireds.""" return self._state.color_temperature @@ -145,11 +142,11 @@ class EsphomeLight(EsphomeEntity, LightEntity): return self._static_info.effects @property - def min_mireds(self) -> float: + def min_mireds(self) -> float: # type: ignore[override] """Return the coldest color_temp that this light supports.""" return self._static_info.min_mireds @property - def max_mireds(self) -> float: + def max_mireds(self) -> float: # type: ignore[override] """Return the warmest color_temp that this light supports.""" return self._static_info.max_mireds diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 08b31e91b79..91000731483 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import cast from aioesphomeapi import NumberInfo, NumberState import voluptuous as vol @@ -35,26 +36,19 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member -class EsphomeNumber(EsphomeEntity, NumberEntity): +class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """A number implementation for esphome.""" - @property - def _static_info(self) -> NumberInfo: - return super()._static_info - - @property - def _state(self) -> NumberState | None: - return super()._state - @property def icon(self) -> str | None: """Return the icon.""" if not self._static_info.icon: return None - return ICON_SCHEMA(self._static_info.icon) + return cast(str, ICON_SCHEMA(self._static_info.icon)) @property def min_value(self) -> float: @@ -72,7 +66,7 @@ class EsphomeNumber(EsphomeEntity, NumberEntity): return super()._static_info.step @esphome_state_property - def value(self) -> float: + def value(self) -> float | None: """Return the state of the entity.""" if math.isnan(self._state.state): return None diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index d3dce2dea1b..48e32809456 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import cast from aioesphomeapi import ( SensorInfo, @@ -21,6 +22,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt from . import ( @@ -34,7 +36,7 @@ ICON_SCHEMA = vol.Schema(cv.icon) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up esphome sensors based on a config entry.""" await platform_async_setup_entry( @@ -58,10 +60,11 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member -_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass] = EsphomeEnumMapper( +_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMapper( { SensorStateClass.NONE: None, SensorStateClass.MEASUREMENT: STATE_CLASS_MEASUREMENT, @@ -69,23 +72,15 @@ _STATE_CLASSES: EsphomeEnumMapper[SensorStateClass] = EsphomeEnumMapper( ) -class EsphomeSensor(EsphomeEntity, SensorEntity): +class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): """A sensor implementation for esphome.""" @property - def _static_info(self) -> SensorInfo: - return super()._static_info - - @property - def _state(self) -> SensorState | None: - return super()._state - - @property - def icon(self) -> str: + def icon(self) -> str | None: """Return the icon.""" if not self._static_info.icon or self._static_info.device_class: return None - return ICON_SCHEMA(self._static_info.icon) + return cast(str, ICON_SCHEMA(self._static_info.icon)) @property def force_update(self) -> bool: @@ -104,14 +99,14 @@ class EsphomeSensor(EsphomeEntity, SensorEntity): return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if not self._static_info.unit_of_measurement: return None return self._static_info.unit_of_measurement @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" if self._static_info.device_class not in DEVICE_CLASSES: return None @@ -125,17 +120,9 @@ class EsphomeSensor(EsphomeEntity, SensorEntity): return _STATE_CLASSES.from_esphome(self._static_info.state_class) -class EsphomeTextSensor(EsphomeEntity, SensorEntity): +class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): """A text sensor implementation for ESPHome.""" - @property - def _static_info(self) -> TextSensorInfo: - return super()._static_info - - @property - def _state(self) -> TextSensorState | None: - return super()._state - @property def icon(self) -> str: """Return the icon.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 341068b05ad..c2c88ee9376 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -1,17 +1,20 @@ """Support for ESPHome switches.""" from __future__ import annotations +from typing import Any + from aioesphomeapi import SwitchInfo, SwitchState from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( @@ -25,17 +28,14 @@ async def async_setup_entry( ) -class EsphomeSwitch(EsphomeEntity, SwitchEntity): +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking +# pylint: disable=invalid-overridden-method,no-member + + +class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """A switch implementation for ESPHome.""" - @property - def _static_info(self) -> SwitchInfo: - return super()._static_info - - @property - def _state(self) -> SwitchState | None: - return super()._state - @property def icon(self) -> str: """Return the icon.""" @@ -46,17 +46,15 @@ class EsphomeSwitch(EsphomeEntity, SwitchEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state - # https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property - # pylint: disable=invalid-overridden-method @esphome_state_property - def is_on(self) -> bool | None: + def is_on(self) -> bool | None: # type: ignore[override] """Return true if the switch is on.""" return self._state.state - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self._client.switch_command(self._static_info.key, True) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self._client.switch_command(self._static_info.key, False) diff --git a/mypy.ini b/mypy.ini index 4c693dcc5ee..11330acad6f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -352,6 +352,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.esphome.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fastdotcom.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1153,9 +1164,6 @@ ignore_errors = true [mypy-homeassistant.components.entur_public_transport.*] ignore_errors = true -[mypy-homeassistant.components.esphome.*] -ignore_errors = true - [mypy-homeassistant.components.evohome.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 1873ebd46c1..447bbab51f6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -50,7 +50,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.emonitor.*", "homeassistant.components.enphase_envoy.*", "homeassistant.components.entur_public_transport.*", - "homeassistant.components.esphome.*", "homeassistant.components.evohome.*", "homeassistant.components.filter.*", "homeassistant.components.fints.*", diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 735a02e960c..a5de14d946d 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -48,6 +48,13 @@ def mock_api_connection_error(): yield mock_error +@pytest.fixture(autouse=True) +def mock_setup_entry(): + """Mock setting up a config entry.""" + with patch("homeassistant.components.esphome.async_setup_entry", return_value=True): + yield + + async def test_user_connection_works(hass, mock_client): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( From 2970931d8d5f4c1afc5a4e751c55675c168f55f0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 12 Jul 2021 16:01:58 -0500 Subject: [PATCH 241/818] Use entity class attributes for Plex (#52617) --- homeassistant/components/plex/media_player.py | 53 +++------ homeassistant/components/plex/sensor.py | 110 ++++-------------- 2 files changed, 38 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index f9c40f1edc3..1da71bda6aa 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -119,14 +119,18 @@ class PlexMediaPlayer(MediaPlayerEntity): self.machine_identifier = device.machineIdentifier self.session_device = None - self._available = False self._device_protocol_capabilities = None - self._name = None self._previous_volume_level = 1 # Used in fake muting - self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely + self._attr_available = False + self._attr_should_poll = False + self._attr_state = STATE_IDLE + self._attr_unique_id = ( + f"{self.plex_server.machine_identifier}:{self.machine_identifier}" + ) + # Initializes other attributes self.session = session @@ -180,10 +184,10 @@ class PlexMediaPlayer(MediaPlayerEntity): if not self.session: self.force_idle() if not self.device: - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True try: device_url = self.device.url("/") @@ -207,25 +211,15 @@ class PlexMediaPlayer(MediaPlayerEntity): if self.username and self.username != self.plex_server.owner: # Prepend username for shared/managed clients name_parts.insert(0, self.username) - self._name = NAME_FORMAT.format(" - ".join(name_parts)) + self._attr_name = NAME_FORMAT.format(" - ".join(name_parts)) def force_idle(self): """Force client to idle.""" - self._state = STATE_IDLE + self._attr_state = STATE_IDLE if self.player_source == "session": self.device = None self.session_device = None - self._available = False - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def unique_id(self): - """Return the id of this plex client.""" - return f"{self.plex_server.machine_identifier}:{self.machine_identifier}" + self._attr_available = False @property def session(self): @@ -239,17 +233,7 @@ class PlexMediaPlayer(MediaPlayerEntity): self.session_device = self.session.player self.update_state(self.session.state) else: - self._state = STATE_IDLE - - @property - def available(self): - """Return the availability of the client.""" - return self._available - - @property - def name(self): - """Return the name of the device.""" - return self._name + self._attr_state = STATE_IDLE @property @needs_session @@ -257,22 +241,17 @@ class PlexMediaPlayer(MediaPlayerEntity): """Return the username of the client owner.""" return self.session.username - @property - def state(self): - """Return the state of the device.""" - return self._state - def update_state(self, state): """Set the state of the device, handle session termination.""" if state == "playing": - self._state = STATE_PLAYING + self._attr_state = STATE_PLAYING elif state == "paused": - self._state = STATE_PAUSED + self._attr_state = STATE_PAUSED elif state == "stopped": self.session = None self.force_idle() else: - self._state = STATE_IDLE + self._attr_state = STATE_IDLE @property def _is_player_active(self): diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 95ba0a65ef0..2bfd0d0d926 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -57,10 +57,13 @@ class PlexSensor(SensorEntity): def __init__(self, hass, plex_server): """Initialize the sensor.""" - self._state = None + self._attr_icon = "mdi:plex" + self._attr_name = NAME_FORMAT.format(plex_server.friendly_name) + self._attr_should_poll = False + self._attr_unique_id = f"sensor-{plex_server.machine_identifier}" + self._attr_unit_of_measurement = "Watching" + self._server = plex_server - self._name = NAME_FORMAT.format(plex_server.friendly_name) - self._unique_id = f"sensor-{plex_server.machine_identifier}" self.async_refresh_sensor = Debouncer( hass, _LOGGER, @@ -83,39 +86,9 @@ class PlexSensor(SensorEntity): async def _async_refresh_sensor(self): """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) - self._state = len(self._server.sensor_attributes) + self._attr_state = len(self._server.sensor_attributes) self.async_write_ha_state() - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the id of this plex client.""" - return self._unique_id - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return "Watching" - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:plex" - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -146,11 +119,15 @@ class PlexLibrarySectionSensor(SensorEntity): self.server_id = plex_server.machine_identifier self.library_section = plex_library_section self.library_type = plex_library_section.type - self._name = f"{self.server_name} Library - {plex_library_section.title}" - self._unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" - self._state = None - self._available = True - self._attributes = {} + + self._attr_available = True + self._attr_entity_registry_enabled_default = False + self._attr_extra_state_attributes = {} + self._attr_icon = LIBRARY_ICON_LOOKUP.get(self.library_type, "mdi:plex") + self._attr_name = f"{self.server_name} Library - {plex_library_section.title}" + self._attr_should_poll = False + self._attr_unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" + self._attr_unit_of_measurement = "Items" async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -168,9 +145,9 @@ class PlexLibrarySectionSensor(SensorEntity): _LOGGER.debug("Refreshing library sensor for '%s'", self.name) try: await self.hass.async_add_executor_job(self._update_state_and_attrs) - self._available = True + self._attr_available = True except NotFound: - self._available = False + self._attr_available = False self.async_write_ha_state() def _update_state_and_attrs(self): @@ -179,59 +156,16 @@ class PlexLibrarySectionSensor(SensorEntity): self.library_type, self.library_type ) - self._state = self.library_section.totalViewSize( + self._attr_state = self.library_section.totalViewSize( libtype=primary_libtype, includeCollections=False ) for libtype in LIBRARY_ATTRIBUTE_TYPES.get(self.library_type, []): - self._attributes[f"{libtype}s"] = self.library_section.totalViewSize( + self._attr_extra_state_attributes[ + f"{libtype}s" + ] = self.library_section.totalViewSize( libtype=libtype, includeCollections=False ) - @property - def available(self): - """Return the availability of the client.""" - return self._available - - @property - def entity_registry_enabled_default(self): - """Return if sensor should be enabled by default.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the id of this plex client.""" - return self._unique_id - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return "Items" - - @property - def icon(self): - """Return the icon of the sensor.""" - return LIBRARY_ICON_LOOKUP.get(self.library_type, "mdi:plex") - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - @property def device_info(self): """Return a device description for device registry.""" From 9864f2ef8be676dc4aa8ed03e63594199895a60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 13 Jul 2021 01:12:55 +0300 Subject: [PATCH 242/818] String formatting cleanups (#52937) --- homeassistant/components/buienradar/util.py | 2 +- homeassistant/components/cisco_ios/device_tracker.py | 2 +- homeassistant/components/etherscan/sensor.py | 2 +- homeassistant/components/google_travel_time/helpers.py | 2 +- homeassistant/components/mailbox/__init__.py | 3 +-- homeassistant/components/mobile_app/webhook.py | 2 +- homeassistant/components/mochad/switch.py | 2 +- homeassistant/components/nest/legacy/sensor.py | 4 ++-- homeassistant/components/netio/switch.py | 8 ++++---- homeassistant/components/octoprint/__init__.py | 4 ++-- homeassistant/components/openhardwaremonitor/sensor.py | 2 +- homeassistant/components/rss_feed_template/__init__.py | 2 +- homeassistant/components/smtp/notify.py | 2 +- homeassistant/components/statsd/__init__.py | 2 +- homeassistant/components/xiaomi_aqara/light.py | 2 +- homeassistant/util/async_.py | 2 +- homeassistant/util/ruamel_yaml.py | 2 +- tests/auth/test_init.py | 2 +- tests/components/foobot/test_sensor.py | 2 +- tests/components/hyperion/test_light.py | 2 +- tests/components/mailbox/test_init.py | 10 +++++----- tests/components/statsd/test_init.py | 6 ++---- 22 files changed, 32 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 83c511713d0..8934a7a6833 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -99,7 +99,7 @@ class BrData: return result except (asyncio.TimeoutError, aiohttp.ClientError) as err: - result[MESSAGE] = "%s" % err + result[MESSAGE] = str(err) return result finally: if resp is not None: diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index b9eadec7d18..b30e9dae1f3 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -118,7 +118,7 @@ class CiscoDeviceScanner(DeviceScanner): router_hostname = initial_line[len(initial_line) - 1] router_hostname += "#" # Set the discovered hostname as prompt - regex_expression = ("(?i)^%s" % router_hostname).encode() + regex_expression = f"(?i)^{router_hostname}".encode() cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE) # Allow full arp table to print at once cisco_ssh.sendline("terminal length 0") diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 1fa2edbf2e8..1b10cc39fe1 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -34,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if token: token = token.upper() if not name: - name = "%s Balance" % token + name = f"{token} Balance" if not name: name = "ETH Balance" diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 425d21ee181..cf5f6e8b0af 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -41,7 +41,7 @@ def get_location_from_entity(hass, logger, entity_id): return get_location_from_attributes(entity) # Check if device is in a zone - zone_entity = hass.states.get("zone.%s" % entity.state) + zone_entity = hass.states.get(f"zone.{entity.state}") if location.has_location(zone_entity): logger.debug( "%s is in %s, getting zone location", entity_id, zone_entity.entity_id diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 5d05596fb23..307bd54195f 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -254,8 +254,7 @@ class MailboxMediaView(MailboxView): try: stream = await mailbox.async_get_media(msgid) except StreamError as err: - error_msg = "Error getting media: %s" % (err) - _LOGGER.error(error_msg) + _LOGGER.error("Error getting media: %s", err) return web.Response(status=HTTP_INTERNAL_SERVER_ERROR) if stream: return web.Response(body=stream, content_type=mailbox.media_type) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 64f10d5616a..99bb153f3ee 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -268,7 +268,7 @@ async def webhook_stream_camera(hass, config_entry, data): status=HTTP_BAD_REQUEST, ) - resp = {"mjpeg_path": "/api/camera_proxy_stream/%s" % (camera.entity_id)} + resp = {"mjpeg_path": f"/api/camera_proxy_stream/{camera.entity_id}"} if camera.attributes[ATTR_SUPPORTED_FEATURES] & CAMERA_SUPPORT_STREAM: try: diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index e7f1bee99f6..d23d46c8392 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -44,7 +44,7 @@ class MochadSwitch(SwitchEntity): self._controller = ctrl self._address = dev[CONF_ADDRESS] - self._name = dev.get(CONF_NAME, "x10_switch_dev_%s" % self._address) + self._name = dev.get(CONF_NAME, f"x10_switch_dev_{self._address}") self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.switch = device.Device(ctrl, self._address, comm_type=self._comm_type) # Init with false to avoid locking HA for long on CM19A (goes from rf diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 53d9c824466..54df3921bbd 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -93,9 +93,9 @@ async def async_setup_legacy_entry(hass, entry, async_add_entities): if variable in _SENSOR_TYPES_DEPRECATED: if variable in DEPRECATED_WEATHER_VARS: wstr = ( - "Nest no longer provides weather data like %s. See " + f"Nest no longer provides weather data like {variable}. See " "https://www.home-assistant.io/integrations/#weather " - "for a list of other weather integrations to use." % variable + "for a list of other weather integrations to use." ) else: wstr = ( diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index a254d06fc06..c39b1598c89 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -97,12 +97,12 @@ class NetioApiView(HomeAssistantView): for i in range(1, 5): out = "output%d" % i - states.append(data.get("%s_state" % out) == STATE_ON) - consumptions.append(float(data.get("%s_consumption" % out, 0))) + states.append(data.get(f"{out}_state") == STATE_ON) + consumptions.append(float(data.get(f"{out}_consumption", 0))) cumulated_consumptions.append( - float(data.get("%s_cumulatedConsumption" % out, 0)) / 1000 + float(data.get(f"{out}_cumulatedConsumption", 0)) / 1000 ) - start_dates.append(data.get("%s_consumptionStart" % out, "")) + start_dates.append(data.get(f"{out}_consumptionStart", "")) _LOGGER.debug( "%s: %s, %s, %s since %s", diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 918f0258f78..396a18318f2 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -241,7 +241,7 @@ class OctoPrintAPI: return response.json() except requests.ConnectionError as exc_con: - log_string = "Failed to connect to Octoprint server. Error: %s" % exc_con + log_string = f"Failed to connect to Octoprint server. Error: {exc_con}" if not self.available_error_logged: _LOGGER.error(log_string) @@ -254,7 +254,7 @@ class OctoPrintAPI: except requests.HTTPError as ex_http: status_code = ex_http.response.status_code - log_string = "Failed to update OctoPrint status. Error: %s" % ex_http + log_string = f"Failed to update OctoPrint status. Error: {ex_http}" # Only log the first failure if endpoint == "job": log_string = f"Endpoint: job {log_string}" diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 70d0d36176c..8d14074892d 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -109,7 +109,7 @@ class OpenHardwareMonitorDevice(SensorEntity): self.attributes = _attributes return array = array[path_number][OHM_CHILDREN] - _attributes.update({"level_%s" % path_index: values[OHM_NAME]}) + _attributes.update({f"level_{path_index}": values[OHM_NAME]}) class OpenHardwareMonitorData: diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index c9871c8f6b5..4ea9c27b82e 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -40,7 +40,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the RSS feed template component.""" for (feeduri, feedconfig) in config[DOMAIN].items(): - url = "/api/rss_template/%s" % feeduri + url = f"/api/rss_template/{feeduri}" requires_auth = feedconfig.get("requires_api_password") diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 29f0eb777ba..0586a5838fc 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -241,7 +241,7 @@ def _attach_file(atch_name, content_id): atch_name, ) attachment = MIMEApplication(file_bytes, Name=atch_name) - attachment["Content-Disposition"] = "attachment; " 'filename="%s"' % atch_name + attachment["Content-Disposition"] = f'attachment; filename="{atch_name}"' attachment.add_header("Content-ID", f"<{content_id}>") return attachment diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index 7ca3068f003..b4657838683 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -74,7 +74,7 @@ def setup(hass, config): if show_attribute_flag is True: if isinstance(_state, (float, int)): - statsd_client.gauge("%s.state" % state.entity_id, _state, sample_rate) + statsd_client.gauge(f"{state.entity_id}.state", _state, sample_rate) # Send attribute values for key, value in states.items(): diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 494c9af920e..30f72a7ba59 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -58,7 +58,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): self._state = False return True - rgbhexstr = "%x" % value + rgbhexstr = f"{value:x}" if len(rgbhexstr) > 8: _LOGGER.error( "Light RGB data error." diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index a467d544174..86308b48f7a 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -30,7 +30,7 @@ def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: raise RuntimeError("Cannot be called from within the event loop") if not coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required: %s" % coro) + raise TypeError(f"A coroutine object is required: {coro}") def callback() -> None: """Handle the firing of a coroutine.""" diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index b9f69b15578..8d813eaa5a4 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -47,7 +47,7 @@ def _include_yaml( """ if constructor.name is None: raise HomeAssistantError( - "YAML include error: filename not set for %s" % node.value + f"YAML include error: filename not set for {node.value}" ) fname = os.path.join(os.path.dirname(constructor.name), node.value) return load_yaml(fname, False) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 0128c9794f3..4a763a6e995 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -335,7 +335,7 @@ async def test_saving_loading(hass, hass_storage): assert r_token.last_used_at is None assert r_token.last_used_ip is None else: - assert False, "Unknown client_id: %s" % r_token.client_id + assert False, f"Unknown client_id: {r_token.client_id}" async def test_cannot_retrieve_expired_access_token(hass): diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index f817b38c98b..f3bf961fdc8 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -51,7 +51,7 @@ async def test_default_setup(hass, aioclient_mock): } for name, value in metrics.items(): - state = hass.states.get("sensor.foobot_happybot_%s" % name) + state = hass.states.get(f"sensor.foobot_happybot_{name}") assert state.state == value[0] assert state.attributes.get("unit_of_measurement") == value[1] diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 829a76f22d3..5d57d3be70d 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1384,7 +1384,7 @@ async def test_deprecated_effect_names(caplog, hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: component}, blocking=True, ) - assert "Use of Hyperion effect '%s' is deprecated" % component in caplog.text + assert f"Use of Hyperion effect '{component}' is deprecated" in caplog.text # Simulate a state callback from Hyperion. client.visible_priority = { diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 75ecc8d9db3..8f75085f9ee 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -43,7 +43,7 @@ async def test_get_media_from_mailbox(mock_http_client): msgtxt = "Message 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgsha = sha1(msgtxt.encode("utf-8")).hexdigest() - url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) + url = f"/api/mailbox/media/DemoMailbox/{msgsha}" req = await mock_http_client.get(url) assert req.status == 200 data = await req.read() @@ -58,7 +58,7 @@ async def test_delete_from_mailbox(mock_http_client): msgsha2 = sha1(msgtxt2.encode("utf-8")).hexdigest() for msg in [msgsha1, msgsha2]: - url = "/api/mailbox/delete/DemoMailbox/%s" % (msg) + url = f"/api/mailbox/delete/DemoMailbox/{msg}" req = await mock_http_client.delete(url) assert req.status == 200 @@ -80,7 +80,7 @@ async def test_get_messages_from_invalid_mailbox(mock_http_client): async def test_get_media_from_invalid_mailbox(mock_http_client): """Get messages from mailbox.""" msgsha = "0000000000000000000000000000000000000000" - url = "/api/mailbox/media/mailbox.invalid_mailbox/%s" % (msgsha) + url = f"/api/mailbox/media/mailbox.invalid_mailbox/{msgsha}" req = await mock_http_client.get(url) assert req.status == HTTP_NOT_FOUND @@ -89,7 +89,7 @@ async def test_get_media_from_invalid_mailbox(mock_http_client): async def test_get_media_from_invalid_msgid(mock_http_client): """Get messages from mailbox.""" msgsha = "0000000000000000000000000000000000000000" - url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) + url = f"/api/mailbox/media/DemoMailbox/{msgsha}" req = await mock_http_client.get(url) assert req.status == HTTP_INTERNAL_SERVER_ERROR @@ -98,7 +98,7 @@ async def test_get_media_from_invalid_msgid(mock_http_client): async def test_delete_from_invalid_mailbox(mock_http_client): """Get audio from mailbox.""" msgsha = "0000000000000000000000000000000000000000" - url = "/api/mailbox/delete/mailbox.invalid_mailbox/%s" % (msgsha) + url = f"/api/mailbox/delete/mailbox.invalid_mailbox/{msgsha}" req = await mock_http_client.delete(url) assert req.status == HTTP_NOT_FOUND diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index a0e5fd51669..62808491c2d 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -110,10 +110,8 @@ async def test_event_listener_attr_details(hass, mock_client): handler_method(MagicMock(data={"new_state": state})) mock_client.gauge.assert_has_calls( [ - mock.call("%s.state" % state.entity_id, out, statsd.DEFAULT_RATE), - mock.call( - "%s.attribute_key" % state.entity_id, 3.2, statsd.DEFAULT_RATE - ), + mock.call(f"{state.entity_id}.state", out, statsd.DEFAULT_RATE), + mock.call(f"{state.entity_id}.attribute_key", 3.2, statsd.DEFAULT_RATE), ] ) From fe5abf1a87bf4c38a6d2ac259dbee5002a51f93b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 12 Jul 2021 18:22:50 -0400 Subject: [PATCH 243/818] Use entity class attributes for aqualogic (#52668) --- homeassistant/components/aqualogic/sensor.py | 50 +++++--------------- homeassistant/components/aqualogic/switch.py | 14 ++---- 2 files changed, 15 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 1608f6173d8..01f31757c9d 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -69,46 +69,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AquaLogicSensor(SensorEntity): """Sensor implementation for the AquaLogic component.""" + _attr_should_poll = False + def __init__(self, processor, sensor_type): """Initialize sensor.""" self._processor = processor self._type = sensor_type - self._state = None - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def name(self): - """Return the name of the sensor.""" - return f"AquaLogic {SENSOR_TYPES[self._type][0]}" - - @property - def unit_of_measurement(self): - """Return the unit of measurement the value is expressed in.""" - panel = self._processor.panel - if panel is None: - return None - if panel.is_metric: - return SENSOR_TYPES[self._type][1][0] - return SENSOR_TYPES[self._type][1][1] - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES[self._type][3] - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self._type][2] + self._attr_name = f"AquaLogic {SENSOR_TYPES[sensor_type][0]}" + self._attr_icon = SENSOR_TYPES[sensor_type][2] async def async_added_to_hass(self): """Register callbacks.""" @@ -123,5 +91,11 @@ class AquaLogicSensor(SensorEntity): """Update callback.""" panel = self._processor.panel if panel is not None: - self._state = getattr(panel, self._type) - self.async_write_ha_state() + if panel.is_metric: + self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0] + self._attr_state = getattr(panel, self._type) + self.async_write_ha_state() + else: + self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1] + else: + self._attr_unit_of_measurement = None diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 08bba4cbd2d..c05bacc5f03 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -44,10 +44,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AquaLogicSwitch(SwitchEntity): """Switch implementation for the AquaLogic component.""" + _attr_should_poll = False + def __init__(self, processor, switch_type): """Initialize switch.""" self._processor = processor - self._type = switch_type self._state_name = { "lights": States.LIGHTS, "filter": States.FILTER, @@ -60,16 +61,7 @@ class AquaLogicSwitch(SwitchEntity): "aux_6": States.AUX_6, "aux_7": States.AUX_7, }[switch_type] - - @property - def name(self): - """Return the name of the switch.""" - return f"AquaLogic {SWITCH_TYPES[self._type]}" - - @property - def should_poll(self): - """Return the polling state.""" - return False + self._attr_name = f"AquaLogic {SWITCH_TYPES[switch_type]}" @property def is_on(self): From 4afede9e0899db4e64b438fe6e08140db8abb1eb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 13 Jul 2021 00:27:48 +0200 Subject: [PATCH 244/818] Add schedule selector for Netatmo (#52909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/netatmo/const.py | 3 +- homeassistant/components/netatmo/select.py | 163 +++++++++++++++++++++ tests/components/netatmo/test_select.py | 65 ++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/netatmo/select.py create mode 100644 tests/components/netatmo/test_select.py diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index e974d43134e..8b2fb8701da 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -2,6 +2,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN API = "api" @@ -10,7 +11,7 @@ DOMAIN = "netatmo" MANUFACTURER = "Netatmo" DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" -PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN] +PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN] MODEL_NAPLUG = "Relay" MODEL_NATHERM1 = "Smart Thermostat" diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py new file mode 100644 index 00000000000..726ae919099 --- /dev/null +++ b/homeassistant/components/netatmo/select.py @@ -0,0 +1,163 @@ +"""Support for the Netatmo climate schedule selector.""" +from __future__ import annotations + +import logging +from typing import cast + +import pyatmo + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .climate import get_all_home_ids +from .const import ( + DATA_HANDLER, + DATA_SCHEDULES, + DOMAIN, + EVENT_TYPE_SCHEDULE, + MANUFACTURER, + SIGNAL_NAME, +) +from .data_handler import HOMEDATA_DATA_CLASS_NAME, NetatmoDataHandler +from .netatmo_entity_base import NetatmoBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Netatmo energy platform schedule selector.""" + data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] + + await data_handler.register_data_class( + HOMEDATA_DATA_CLASS_NAME, HOMEDATA_DATA_CLASS_NAME, None + ) + home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + + if not home_data or home_data.raw_data == {}: + raise PlatformNotReady + + if HOMEDATA_DATA_CLASS_NAME not in data_handler.data: + raise PlatformNotReady + + entities = [ + NetatmoScheduleSelect( + data_handler, + home_id, + list(hass.data[DOMAIN][DATA_SCHEDULES][home_id].values()), + ) + for home_id in get_all_home_ids(home_data) + if home_id in hass.data[DOMAIN][DATA_SCHEDULES] + ] + + _LOGGER.debug("Adding climate schedule select entities %s", entities) + async_add_entities(entities, True) + + +class NetatmoScheduleSelect(NetatmoBase, SelectEntity): + """Representation a Netatmo thermostat schedule selector.""" + + def __init__( + self, data_handler: NetatmoDataHandler, home_id: str, options: list + ) -> None: + """Initialize the select entity.""" + SelectEntity.__init__(self) + super().__init__(data_handler) + + self._home_id = home_id + + self._data_classes.extend( + [ + { + "name": HOMEDATA_DATA_CLASS_NAME, + SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, + }, + ] + ) + + self._device_name = self._data.homes[home_id]["name"] + self._attr_name = f"{MANUFACTURER} {self._device_name}" + + self._model: str = "NATherm1" + + self._attr_unique_id = f"{self._home_id}-schedule-select" + + self._attr_current_option = ( + self._data._get_selected_schedule( # pylint: disable=protected-access + home_id=self._home_id + ).get("name") + ) + self._attr_options = options + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + + for event_type in (EVENT_TYPE_SCHEDULE,): + self._listeners.append( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{event_type}", + self.handle_event, + ) + ) + + async def handle_event(self, event: dict) -> None: + """Handle webhook events.""" + data = event["data"] + + if self._home_id != data["home_id"]: + return + + if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: + self._attr_current_option = self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].get(data["schedule_id"]) + self.async_write_ha_state() + + @property + def _data(self) -> pyatmo.AsyncHomeData: + """Return data for this entity.""" + return cast( + pyatmo.AsyncHomeData, + self.data_handler.data[self._data_classes[0]["name"]], + ) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): + if name != option: + continue + _LOGGER.debug( + "Setting %s schedule to %s (%s)", + self._home_id, + option, + sid, + ) + await self._data.async_switch_home_schedule( + home_id=self._home_id, schedule_id=sid + ) + break + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_current_option = ( + self._data._get_selected_schedule( # pylint: disable=protected-access + home_id=self._home_id + ).get("name") + ) + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = { + schedule_id: schedule_data.get("name") + for schedule_id, schedule_data in ( + self._data.schedules[self._home_id].items() + ) + } + self._attr_options = list( + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].values() + ) diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py new file mode 100644 index 00000000000..838b2e2d290 --- /dev/null +++ b/tests/components/netatmo/test_select.py @@ -0,0 +1,65 @@ +"""The tests for the Netatmo climate platform.""" +from unittest.mock import patch + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID, SERVICE_SELECT_OPTION + +from .common import selected_platforms, simulate_webhook + + +async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): + """Test service for selecting Netatmo schedule with thermostats.""" + with selected_platforms(["climate", "select"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + select_entity_livingroom = "select.netatmo_myhome" + + assert hass.states.get(select_entity_livingroom).state == "Default" + assert hass.states.get(select_entity_livingroom).attributes[ATTR_OPTIONS] == [ + "Default", + "Winter", + ] + + # Fake backend response changing schedule + response = { + "event_type": "schedule", + "schedule_id": "b1b54a2f45795764f59d50d8", + "previous_schedule_id": "59d32176d183948b05ab4dce", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(select_entity_livingroom).state == "Winter" + + # Test setting a different schedule + with patch( + "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + ) as mock_switch_home_schedule: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: select_entity_livingroom, + ATTR_OPTION: "Default", + }, + blocking=True, + ) + await hass.async_block_till_done() + mock_switch_home_schedule.assert_called_once_with( + home_id="91763b24c43d3e344f424e8b", schedule_id="591b54a2764ff4d50d8b5795" + ) + + # Fake backend response changing schedule + response = { + "event_type": "schedule", + "schedule_id": "591b54a2764ff4d50d8b5795", + "previous_schedule_id": "b1b54a2f45795764f59d50d8", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(select_entity_livingroom).state == "Default" From 6723942bf8fca901bb837b403a82c430ed0b1d40 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jul 2021 00:13:03 +0000 Subject: [PATCH 245/818] [ci skip] Translation update --- .../components/huawei_lte/translations/en.json | 5 +++-- .../components/huawei_lte/translations/he.json | 2 +- .../components/huawei_lte/translations/nl.json | 3 ++- .../components/huawei_lte/translations/ru.json | 5 +++-- .../huawei_lte/translations/zh-Hant.json | 5 +++-- .../components/zwave_js/translations/en.json | 17 +++++++---------- .../components/zwave_js/translations/he.json | 5 +++++ .../components/zwave_js/translations/nl.json | 7 +++++++ .../components/zwave_js/translations/ru.json | 7 +++++++ .../zwave_js/translations/zh-Hant.json | 7 +++++++ 10 files changed, 45 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index c94791ee592..ade7beed75c 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Username" }, - "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", + "description": "Enter device access details.", "title": "Configure Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices", - "track_wired_clients": "Track wired network clients" + "track_wired_clients": "Track wired network clients", + "unauthenticated_mode": "Unauthenticated mode (change requires reload)" } } } diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json index f55b325d867..1e4333a989a 100644 --- a/homeassistant/components/huawei_lte/translations/he.json +++ b/homeassistant/components/huawei_lte/translations/he.json @@ -18,7 +18,7 @@ "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05d4\u05d6\u05df \u05e4\u05e8\u05d8\u05d9 \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d4\u05ea\u05e7\u05df. \u05e6\u05d9\u05d5\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05d0\u05da \u05de\u05d0\u05e4\u05e9\u05e8 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05ea\u05db\u05d5\u05e0\u05d5\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05e0\u05d5\u05e1\u05e4\u05d5\u05ea. \u05de\u05e6\u05d3 \u05e9\u05e0\u05d9, \u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d7\u05d9\u05d1\u05d5\u05e8 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05dc\u05d5\u05dc \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05d2\u05d9\u05e9\u05d4 \u05dc\u05de\u05de\u05e9\u05e7 \u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05d7\u05d5\u05e5 \u05dc-Home Assistant \u05d1\u05d6\u05de\u05df \u05e9\u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e4\u05e2\u05d9\u05dc, \u05d5\u05dc\u05d4\u05d9\u05e4\u05da." + "description": "\u05d9\u05e9 \u05dc\u05d4\u05d6\u05d9\u05df \u05e4\u05e8\u05d8\u05d9 \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d4\u05ea\u05e7\u05df." } } } diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index d851b239436..715efbfd506 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -35,7 +35,8 @@ "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", "track_new_devices": "Volg nieuwe apparaten", - "track_wired_clients": "Volg bekabelde netwerkclients" + "track_wired_clients": "Volg bekabelde netwerkclients", + "unauthenticated_mode": "Niet-geverifieerde modus (wijzigen vereist opnieuw laden)" } } } diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index 34a00f0523c..e9ddd73191d 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -23,7 +23,7 @@ "url": "URL-\u0430\u0434\u0440\u0435\u0441", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "title": "Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "track_wired_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" + "track_wired_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438", + "unauthenticated_mode": "\u0420\u0435\u0436\u0438\u043c \u0431\u0435\u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 (\u0434\u043b\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430)" } } } diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index 906a4fdc011..0c1d2baa7a9 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -23,7 +23,7 @@ "url": "\u7db2\u5740", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u88dd\u7f6e Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", + "description": "\u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002", "title": "\u8a2d\u5b9a\u83ef\u70ba LTE" } } @@ -35,7 +35,8 @@ "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e", - "track_wired_clients": "\u8ffd\u8e64\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" + "track_wired_clients": "\u8ffd\u8e64\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef", + "unauthenticated_mode": "\u672a\u6388\u6b0a\u6a21\u5f0f\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09" } } } diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index af63014f588..bec90dd50d8 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Config parameter {subtype} value", + "node_status": "Node status", + "value": "Current value of a Z-Wave Value" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", @@ -101,15 +108,5 @@ } } }, - "device_automation": { - "condition_type": { - "alive": "Node is alive", - "asleep": "Node is asleep", - "awake": "Node is awake", - "config_parameter": "Config parameter {subtype} value", - "dead": "Node is dead", - "value": "Current value of a Z-Wave Value" - } - }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/he.json b/homeassistant/components/zwave_js/translations/he.json index 32a2ff96298..7c1cab98854 100644 --- a/homeassistant/components/zwave_js/translations/he.json +++ b/homeassistant/components/zwave_js/translations/he.json @@ -29,6 +29,11 @@ } } }, + "device_automation": { + "condition_type": { + "node_status": "\u05de\u05e6\u05d1 \u05d4\u05e6\u05d5\u05de\u05ea" + } + }, "options": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 26f75148d57..d0475e20a3f 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Config parameter {subtype} waarde", + "node_status": "Knooppuntstatus", + "value": "Huidige waarde van een Z-Wave-waarde" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.", diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 64c8101740b..5dbfd184fdb 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {subtype}", + "node_status": "\u0421\u0442\u0430\u0442\u0443\u0441 \u0443\u0437\u043b\u0430", + "value": "\u0422\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 Z-Wave Value" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 3e43c500c39..e98b218ebc8 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "\u8a2d\u5b9a\u53c3\u6578 {subtype} \u6578\u503c", + "node_status": "\u7bc0\u9ede\u72c0\u614b", + "value": "Z-Wave \u76ee\u524d\u503c" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", From 92e4013f735ef36db017654fb7fe929808c7a415 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Jul 2021 22:18:04 -0400 Subject: [PATCH 246/818] Fix siren turn on parameter filtering (#52947) * Fix siren turn on parameter filtering * fix test --- homeassistant/components/siren/__init__.py | 9 ++++++--- tests/components/demo/test_siren.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index c9550574bb1..f571f87c93e 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -78,10 +78,13 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: siren: SirenEntity, call: ServiceCall ) -> None: """Handle turning a siren on.""" + data = { + k: v + for k, v in call.data.items() + if k in (ATTR_TONE, ATTR_DURATION, ATTR_VOLUME_LEVEL) + } await siren.async_turn_on( - **filter_turn_on_params( - siren, cast(SirenTurnOnServiceParameters, dict(call.data)) - ) + **filter_turn_on_params(siren, cast(SirenTurnOnServiceParameters, data)) ) component.async_register_entity_service( diff --git a/tests/components/demo/test_siren.py b/tests/components/demo/test_siren.py index 74c39c668e9..117343d2958 100644 --- a/tests/components/demo/test_siren.py +++ b/tests/components/demo/test_siren.py @@ -105,4 +105,4 @@ async def test_turn_on_strip_attributes(hass): blocking=True, ) assert svc_call.called - assert svc_call.call_args_list[0] == call(**{ATTR_ENTITY_ID: [ENTITY_SIREN]}) + assert svc_call.call_args_list[0] == call() From e915f5be53667de0f416344bf6af3d0c20e97c39 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jul 2021 00:13:43 -0400 Subject: [PATCH 247/818] Handle FailedCommand exceptions in zwave_js WS API (#52461) * Handle zwave-js errors in WS API * Unsubscribe callbacks when zwave-js error is caught * fix tests * simplify unsub logic * add tests * add kwargs to be safe * use existing msg format * switch to generic failed command handling * remove unneeded constant * Update homeassistant/components/zwave_js/api.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/api.py Co-authored-by: Martin Hjelmare * fix Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 68 +++- tests/components/zwave_js/test_api.py | 470 ++++++++++++++++++++++- 2 files changed, 507 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5cf98d44802..1c303eea053 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -4,7 +4,7 @@ from __future__ import annotations import dataclasses from functools import partial, wraps import json -from typing import Callable +from typing import Any, Callable from aiohttp import hdrs, web, web_exceptions, web_request import voluptuous as vol @@ -13,6 +13,7 @@ from zwave_js_server.client import Client from zwave_js_server.const import CommandClass, LogLevel from zwave_js_server.exceptions import ( BaseZwaveJSServerError, + FailedCommand, InvalidNewValue, NotFoundError, SetValueFailed, @@ -47,6 +48,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, + DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, ) @@ -134,6 +136,30 @@ def async_get_node(orig_func: Callable) -> Callable: return async_get_node_func +def async_handle_failed_command(orig_func: Callable) -> Callable: + """Decorate async function to handle FailedCommand and send relevant error.""" + + @wraps(orig_func) + async def async_handle_failed_command_func( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + *args: Any, + **kwargs: Any, + ) -> None: + """Handle FailedCommand within function and send relevant error.""" + try: + await orig_func(hass, connection, msg, *args, **kwargs) + except FailedCommand as err: + # Unsubscribe to callbacks + if unsubs := msg.get(DATA_UNSUBSCRIBE): + for unsub in unsubs: + unsub() + connection.send_error(msg[ID], err.error_code, err.args[0]) + + return async_handle_failed_command_func + + @callback def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" @@ -318,6 +344,7 @@ async def websocket_node_metadata( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_ping_node( hass: HomeAssistant, @@ -342,6 +369,7 @@ async def websocket_ping_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_add_node( hass: HomeAssistant, @@ -410,7 +438,7 @@ async def websocket_add_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("inclusion started", forward_event), controller.on("inclusion failed", forward_event), controller.on("inclusion stopped", forward_event), @@ -435,6 +463,7 @@ async def websocket_add_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_stop_inclusion( hass: HomeAssistant, @@ -460,6 +489,7 @@ async def websocket_stop_inclusion( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_stop_exclusion( hass: HomeAssistant, @@ -485,6 +515,7 @@ async def websocket_stop_exclusion( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_remove_node( hass: HomeAssistant, @@ -522,7 +553,7 @@ async def websocket_remove_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("exclusion started", forward_event), controller.on("exclusion failed", forward_event), controller.on("exclusion stopped", forward_event), @@ -546,6 +577,7 @@ async def websocket_remove_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_replace_failed_node( hass: HomeAssistant, @@ -628,7 +660,7 @@ async def websocket_replace_failed_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("inclusion started", forward_event), controller.on("inclusion failed", forward_event), controller.on("inclusion stopped", forward_event), @@ -655,6 +687,7 @@ async def websocket_replace_failed_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_remove_failed_node( hass: HomeAssistant, @@ -670,7 +703,8 @@ async def websocket_remove_failed_node( @callback def async_cleanup() -> None: """Remove signal listeners.""" - unsub() + for unsub in unsubs: + unsub() @callback def node_removed(event: dict) -> None: @@ -686,7 +720,7 @@ async def websocket_remove_failed_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsub = controller.on("node removed", node_removed) + msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)] result = await controller.async_remove_failed_node(node_id) connection.send_result( @@ -703,6 +737,7 @@ async def websocket_remove_failed_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_begin_healing_network( hass: HomeAssistant, @@ -755,7 +790,7 @@ async def websocket_subscribe_heal_network_progress( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("heal network progress", partial(forward_event, "progress")), controller.on("heal network done", partial(forward_event, "result")), ] @@ -771,6 +806,7 @@ async def websocket_subscribe_heal_network_progress( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_stop_healing_network( hass: HomeAssistant, @@ -797,6 +833,7 @@ async def websocket_stop_healing_network( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_heal_node( hass: HomeAssistant, @@ -824,6 +861,7 @@ async def websocket_heal_node( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_refresh_node_info( hass: HomeAssistant, @@ -854,7 +892,7 @@ async def websocket_refresh_node_info( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ node.on("interview started", forward_event), node.on("interview completed", forward_event), node.on("interview stage completed", forward_stage), @@ -874,6 +912,7 @@ async def websocket_refresh_node_info( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_refresh_node_values( hass: HomeAssistant, @@ -896,6 +935,7 @@ async def websocket_refresh_node_values( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_refresh_node_cc_values( hass: HomeAssistant, @@ -930,6 +970,7 @@ async def websocket_refresh_node_cc_values( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_set_config_parameter( hass: HomeAssistant, @@ -1027,6 +1068,7 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_subscribe_log_updates( hass: HomeAssistant, @@ -1076,7 +1118,7 @@ async def websocket_subscribe_log_updates( ) ) - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ driver.on("logging", log_messages), driver.on("log config updated", log_config_updates), ] @@ -1114,6 +1156,7 @@ async def websocket_subscribe_log_updates( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_update_log_config( hass: HomeAssistant, @@ -1161,6 +1204,7 @@ async def websocket_get_log_config( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_update_data_collection_preference( hass: HomeAssistant, @@ -1191,6 +1235,7 @@ async def websocket_update_data_collection_preference( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_data_collection_status( hass: HomeAssistant, @@ -1273,6 +1318,7 @@ async def websocket_version_info( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_abort_firmware_update( hass: HomeAssistant, @@ -1337,7 +1383,7 @@ async def websocket_subscribe_firmware_update_status( ) ) - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ node.on("firmware update progress", forward_progress), node.on("firmware update finished", forward_finished), ] @@ -1400,6 +1446,7 @@ class FirmwareUploadView(HomeAssistantView): } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_check_for_config_updates( hass: HomeAssistant, @@ -1427,6 +1474,7 @@ async def websocket_check_for_config_updates( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_install_config_update( hass: HomeAssistant, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index b6d846898d3..80fe6da90f5 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -7,6 +7,7 @@ from zwave_js_server.const import LogLevel from zwave_js_server.event import Event from zwave_js_server.exceptions import ( FailedCommand, + FailedZWaveCommand, InvalidNewValue, NotFoundError, SetValueFailed, @@ -280,13 +281,32 @@ async def test_ping_node( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_ping", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/ping_node", ENTRY_ID: entry.entry_id, NODE_ID: node.node_id, @@ -385,12 +405,30 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "interview failed" + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_begin_inclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( - {ID: 4, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + {ID: 5, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -419,12 +457,48 @@ async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_cli msg = await ws_client.receive_json() assert msg["success"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_stop_inclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/stop_inclusion", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_stop_exclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/stop_exclusion", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( - {ID: 6, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} + {ID: 8, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -432,7 +506,7 @@ async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_cli assert msg["error"]["code"] == ERR_NOT_LOADED await ws_client.send_json( - {ID: 7, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} + {ID: 9, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -494,12 +568,30 @@ async def test_remove_node( ) assert device is None + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_begin_exclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/remove_node", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( - {ID: 4, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} + {ID: 5, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -641,13 +733,32 @@ async def test_replace_failed_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "interview failed" + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_replace_failed_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/replace_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -705,13 +816,32 @@ async def test_remove_failed_node( ) assert device is None + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_remove_failed_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/remove_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/remove_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -747,13 +877,31 @@ async def test_begin_healing_network( assert msg["success"] assert msg["result"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_begin_healing_network", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/begin_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/begin_healing_network", ENTRY_ID: entry.entry_id, } @@ -837,13 +985,31 @@ async def test_stop_healing_network( assert msg["success"] assert msg["result"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_stop_healing_network", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/stop_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/stop_healing_network", ENTRY_ID: entry.entry_id, } @@ -879,13 +1045,32 @@ async def test_heal_node( assert msg["success"] assert msg["result"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_heal_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/heal_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -978,13 +1163,32 @@ async def test_refresh_node_info( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_refresh_info", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/refresh_node_info", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1048,6 +1252,42 @@ async def test_refresh_node_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_refresh_values", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/refresh_node_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/refresh_node_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_refresh_node_cc_values( hass, client, multisensor_6, integration, hass_ws_client @@ -1105,13 +1345,33 @@ async def test_refresh_node_cc_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_refresh_cc_values", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/refresh_node_cc_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + COMMAND_CLASS_ID: 112, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/refresh_node_cc_values", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1307,13 +1567,35 @@ async def test_set_config_parameter( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "homeassistant.components.zwave_js.api.async_set_config_parameter", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 7, + ID: 8, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1625,12 +1907,30 @@ async def test_subscribe_log_updates(hass, integration, client, hass_ws_client): }, } + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_start_listening_logs", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_log_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( - {ID: 2, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id} + {ID: 3, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -1757,13 +2057,32 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): and "must be provided if logging to file" in msg["error"]["message"] ) + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_update_log_config", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LEVEL: "Error"}, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 7, + ID: 8, TYPE: "zwave_js/update_log_config", ENTRY_ID: entry.entry_id, CONFIG: {LEVEL: "Error"}, @@ -1884,13 +2203,50 @@ async def test_data_collection(hass, client, integration, hass_ws_client): client.async_send_command.reset_mock() + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_is_statistics_enabled", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/data_collection_status", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_enable_statistics", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/update_data_collection_preference", + ENTRY_ID: entry.entry_id, + OPTED_IN: True, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 4, + ID: 6, TYPE: "zwave_js/data_collection_status", ENTRY_ID: entry.entry_id, } @@ -1902,7 +2258,7 @@ async def test_data_collection(hass, client, integration, hass_ws_client): await ws_client.send_json( { - ID: 5, + ID: 7, TYPE: "zwave_js/update_data_collection_preference", ENTRY_ID: entry.entry_id, OPTED_IN: True, @@ -1938,6 +2294,42 @@ async def test_abort_firmware_update( assert args["command"] == "node.abort_firmware_update" assert args["nodeId"] == multisensor_6.node_id + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_abort_firmware_update", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_abort_firmware_update_failures( hass, integration, multisensor_6, client, hass_ws_client @@ -2128,13 +2520,31 @@ async def test_check_for_config_updates(hass, client, integration, hass_ws_clien assert config_update["update_available"] assert config_update["new_version"] == "test" + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_check_for_config_updates", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 2, + ID: 3, TYPE: "zwave_js/check_for_config_updates", ENTRY_ID: entry.entry_id, } @@ -2146,7 +2556,7 @@ async def test_check_for_config_updates(hass, client, integration, hass_ws_clien await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/check_for_config_updates", ENTRY_ID: "INVALID", } @@ -2175,13 +2585,31 @@ async def test_install_config_update(hass, client, integration, hass_ws_client): assert msg["result"] assert msg["success"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_install_config_update", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( { - ID: 2, + ID: 3, TYPE: "zwave_js/install_config_update", ENTRY_ID: entry.entry_id, } @@ -2193,7 +2621,7 @@ async def test_install_config_update(hass, client, integration, hass_ws_client): await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/install_config_update", ENTRY_ID: "INVALID", } From 4a058503ca49fa861124fa3bb6abbcd5e8dd0798 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jul 2021 01:02:36 -0400 Subject: [PATCH 248/818] Change behavior of Z-Wave JS services (#52941) * Change behavior of Z-Wave JS services * pop parameters in for loop * Update logger message --- homeassistant/components/zwave_js/helpers.py | 8 +- homeassistant/components/zwave_js/services.py | 50 +++--- tests/components/zwave_js/test_services.py | 148 +++++++++--------- 3 files changed, 103 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index f8e8dab2b46..50d57d9a6e4 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -89,7 +89,7 @@ def async_get_node_from_device_id( device_entry = dev_reg.async_get(device_id) if not device_entry: - raise ValueError("Device ID is not valid") + raise ValueError(f"Device ID {device_id} is not valid") # Use device config entry ID's to validate that this is a valid zwave_js device # and to get the client @@ -107,7 +107,9 @@ def async_get_node_from_device_id( None, ) if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]: - raise ValueError("Device is not from an existing zwave_js config entry") + raise ValueError( + f"Device {device_id} is not from an existing zwave_js config entry" + ) client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] @@ -125,7 +127,7 @@ def async_get_node_from_device_id( node_id = int(identifier[1]) if identifier is not None else None if node_id is None or node_id not in client.driver.controller.nodes: - raise ValueError("Device node can't be found") + raise ValueError(f"Node for device {device_id} can't be found") return client.driver.controller.nodes[node_id] diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 47709a908ed..1204f458bff 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -34,8 +34,9 @@ def parameter_name_does_not_need_bitmask( val: dict[str, int | str | list[str]] ) -> dict[str, int | str | list[str]]: """Validate that if a parameter name is provided, bitmask is not as well.""" - if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and ( - val.get(const.ATTR_CONFIG_PARAMETER_BITMASK) + if ( + isinstance(val[const.ATTR_CONFIG_PARAMETER], str) + and const.ATTR_CONFIG_PARAMETER_BITMASK in val ): raise vol.Invalid( "Don't include a bitmask when a parameter name is specified", @@ -94,25 +95,24 @@ class ZWaveServices: def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]: """Get nodes set from service data.""" nodes: set[ZwaveNode] = set() - try: - if ATTR_ENTITY_ID in val: - nodes |= { + for entity_id in val.pop(ATTR_ENTITY_ID, []): + try: + nodes.add( async_get_node_from_entity_id( self._hass, entity_id, self._ent_reg, self._dev_reg ) - for entity_id in val[ATTR_ENTITY_ID] - } - val.pop(ATTR_ENTITY_ID) - if ATTR_DEVICE_ID in val: - nodes |= { + ) + except ValueError as err: + const.LOGGER.warning(err.args[0]) + for device_id in val.pop(ATTR_DEVICE_ID, []): + try: + nodes.add( async_get_node_from_device_id( self._hass, device_id, self._dev_reg ) - for device_id in val[ATTR_DEVICE_ID] - } - val.pop(ATTR_DEVICE_ID) - except ValueError as err: - raise vol.Invalid(err.args[0]) from err + ) + except ValueError as err: + const.LOGGER.warning(err.args[0]) val[const.ATTR_NODES] = nodes return val @@ -124,22 +124,16 @@ class ZWaveServices: broadcast: bool = val[const.ATTR_BROADCAST] # User must specify a node if they are attempting a broadcast and have more - # than one zwave-js network. We know it's a broadcast if the nodes list is - # empty because of schema validation. + # than one zwave-js network. if ( - not nodes + broadcast + and not nodes and len(self._hass.config_entries.async_entries(const.DOMAIN)) > 1 ): raise vol.Invalid( "You must include at least one entity or device in the service call" ) - # When multicasting, user must specify at least two nodes - if not broadcast and len(nodes) < 2: - raise vol.Invalid( - "To set a value on a single node, use the zwave_js.set_value service" - ) - first_node = next((node for node in nodes), None) # If any nodes don't have matching home IDs, we can't run the command because @@ -413,6 +407,14 @@ class ZWaveServices: nodes = service.data[const.ATTR_NODES] broadcast: bool = service.data[const.ATTR_BROADCAST] + if not broadcast and len(nodes) == 1: + const.LOGGER.warning( + "Passing the zwave_js.multicast_set_value service call to the " + "zwave_js.set_value service since only one node was targeted" + ) + await self.async_set_value(service) + return + value = { "commandClass": service.data[const.ATTR_COMMAND_CLASS], "property": service.data[const.ATTR_PROPERTY], diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index dfc7ddaa85d..25373bcb026 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -261,31 +261,7 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): } assert args["value"] == 1 - # Test that an invalid entity ID raises a MultipleInvalid - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_CONFIG_PARAMETER, - { - ATTR_ENTITY_ID: "sensor.fake_entity", - ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", - ATTR_CONFIG_VALUE: "Fahrenheit", - }, - blocking=True, - ) - - # Test that an invalid device ID raises a MultipleInvalid - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_CONFIG_PARAMETER, - { - ATTR_DEVICE_ID: "fake_device_id", - ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", - ATTR_CONFIG_VALUE: "Fahrenheit", - }, - blocking=True, - ) + client.async_send_command_no_wait.reset_mock() # Test that we can't include a bitmask value if parameter is a string with pytest.raises(vol.Invalid): @@ -308,36 +284,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): identifiers={("test", "test")}, ) - # Test that a non Z-Wave JS device raises a MultipleInvalid - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_CONFIG_PARAMETER, - { - ATTR_DEVICE_ID: non_zwave_js_device.id, - ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", - ATTR_CONFIG_VALUE: "Fahrenheit", - }, - blocking=True, - ) - zwave_js_device_with_invalid_node_id = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")} ) - # Test that a Z-Wave JS device with an invalid node ID raises a MultipleInvalid - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_CONFIG_PARAMETER, - { - ATTR_DEVICE_ID: zwave_js_device_with_invalid_node_id.id, - ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", - ATTR_CONFIG_VALUE: "Fahrenheit", - }, - blocking=True, - ) - non_zwave_js_entity = ent_reg.async_get_or_create( "test", "sensor", @@ -346,18 +296,59 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): config_entry=non_zwave_js_config_entry, ) - # Test that a non Z-Wave JS entity raises a MultipleInvalid - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_CONFIG_PARAMETER, - { - ATTR_ENTITY_ID: non_zwave_js_entity.entity_id, - ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", - ATTR_CONFIG_VALUE: "Fahrenheit", - }, - blocking=True, - ) + # Test that a Z-Wave JS device with an invalid node ID, non Z-Wave JS entity, + # non Z-Wave JS device, invalid device_id, and invalid node_id gets filtered out. + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: [ + AIR_TEMPERATURE_SENSOR, + non_zwave_js_entity.entity_id, + "sensor.fake", + ], + ATTR_DEVICE_ID: [ + zwave_js_device_with_invalid_node_id.id, + non_zwave_js_device.id, + "fake_device_id", + ], + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: "0x01", + ATTR_CONFIG_VALUE: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() # Test that when a device is awake, we call async_send_command instead of # async_send_command_no_wait @@ -862,19 +853,24 @@ async def test_multicast_set_value( client.async_send_command.reset_mock() - # Test sending one node without broadcast fails - with pytest.raises(vol.Invalid): - await hass.services.async_call( - DOMAIN, - SERVICE_MULTICAST_SET_VALUE, - { - ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", - ATTR_VALUE: 2, - }, - blocking=True, - ) + # Test sending one node without broadcast uses the node.set_value command instead + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + + client.async_send_command_no_wait.reset_mock() # Test no device, entity, or broadcast flag raises error with pytest.raises(vol.Invalid): From b49fb1f657482f0bb69a8f43f8644dfdf8e11188 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Jul 2021 08:38:31 +0200 Subject: [PATCH 249/818] Minor test coverage improvement of mfi and zwave sensors (#52935) --- tests/components/mfi/test_sensor.py | 7 ++++++- tests/components/zwave/test_sensor.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 6103c43d3a4..1f5cc5fd04f 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -7,7 +7,7 @@ import requests import homeassistant.components.mfi.sensor as mfi import homeassistant.components.sensor as sensor_component -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from homeassistant.setup import async_setup_component PLATFORM = mfi @@ -133,30 +133,35 @@ async def test_uom_temp(port, sensor): """Test the UOM temperature.""" port.tag = "temperature" assert sensor.unit_of_measurement == TEMP_CELSIUS + assert sensor.device_class == DEVICE_CLASS_TEMPERATURE async def test_uom_power(port, sensor): """Test the UOEM power.""" port.tag = "active_pwr" assert sensor.unit_of_measurement == "Watts" + assert sensor.device_class is None async def test_uom_digital(port, sensor): """Test the UOM digital input.""" port.model = "Input Digital" assert sensor.unit_of_measurement == "State" + assert sensor.device_class is None async def test_uom_unknown(port, sensor): """Test the UOM.""" port.tag = "balloons" assert sensor.unit_of_measurement == "balloons" + assert sensor.device_class is None async def test_uom_uninitialized(port, sensor): """Test that the UOM defaults if not initialized.""" type(port).tag = mock.PropertyMock(side_effect=ValueError) assert sensor.unit_of_measurement == "State" + assert sensor.device_class is None async def test_state_digital(port, sensor): diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index b2cd895df37..ae0fa44ed8c 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -84,6 +84,7 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 191.0 assert device.unit_of_measurement == homeassistant.const.TEMP_FAHRENHEIT + assert device.device_class == homeassistant.const.DEVICE_CLASS_TEMPERATURE value.data = 197.95555 value_changed(value) assert device.state == 198.0 @@ -103,6 +104,7 @@ def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 38.9 assert device.unit_of_measurement == homeassistant.const.TEMP_CELSIUS + assert device.device_class == homeassistant.const.DEVICE_CLASS_TEMPERATURE value.data = 37.95555 value_changed(value) assert device.state == 38.0 @@ -124,6 +126,7 @@ def test_multilevelsensor_value_changed_other_units(mock_openzwave): device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 190.96 assert device.unit_of_measurement == homeassistant.const.ENERGY_KILO_WATT_HOUR + assert device.device_class is None value.data = 197.95555 value_changed(value) assert device.state == 197.96 @@ -143,6 +146,7 @@ def test_multilevelsensor_value_changed_integer(mock_openzwave): device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 5 assert device.unit_of_measurement == "counts" + assert device.device_class is None value.data = 6 value_changed(value) assert device.state == 6 @@ -159,6 +163,7 @@ def test_alarm_sensor_value_changed(mock_openzwave): device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 12.34 assert device.unit_of_measurement == homeassistant.const.PERCENTAGE + assert device.device_class is None value.data = 45.67 value_changed(value) assert device.state == 45.67 From d09035db2a7869762360cf7e5de32a76983686f9 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Tue, 13 Jul 2021 09:41:52 +0200 Subject: [PATCH 250/818] Add switch support to Freedompro (#52727) * Update Freedompro * add test state updates from the API * fix test switch * fix test --- .../components/freedompro/__init__.py | 2 +- homeassistant/components/freedompro/switch.py | 93 +++++++++++++++ tests/components/freedompro/const.py | 2 +- tests/components/freedompro/test_switch.py | 112 ++++++++++++++++++ 4 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/freedompro/switch.py create mode 100644 tests/components/freedompro/test_switch.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 47c0bda1b1c..6e5dd438e2d 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["light"] +PLATFORMS = ["light", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py new file mode 100644 index 00000000000..4e3ffb1a2eb --- /dev/null +++ b/homeassistant/components/freedompro/switch.py @@ -0,0 +1,93 @@ +"""Support for Freedompro switch.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro switch.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] == "switch" or device["type"] == "outlet" + ) + + +class Device(CoordinatorEntity, SwitchEntity): + """Representation of an Freedompro switch.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro switch.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self._attr_name, + "identifiers": { + (DOMAIN, self._attr_unique_id), + }, + "model": self._type, + "manufacturer": "Freedompro", + } + self._attr_is_on = False + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self._attr_unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "on" in state: + self._attr_is_on = state["on"] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_turn_on(self, **kwargs): + """Async function to set on to switch.""" + payload = {"on": True} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self._attr_unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Async function to set off to switch.""" + payload = {"on": False} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self._attr_unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py index 6cf67c932dc..0e8f5c4aa52 100644 --- a/tests/components/freedompro/const.py +++ b/tests/components/freedompro/const.py @@ -39,7 +39,7 @@ DEVICES = [ }, { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W", - "name": "irrigation", + "name": "Irrigation switch", "type": "switch", "characteristics": ["on"], }, diff --git a/tests/components/freedompro/test_switch.py b/tests/components/freedompro/test_switch.py new file mode 100644 index 00000000000..4674b684c41 --- /dev/null +++ b/tests/components/freedompro/test_switch.py @@ -0,0 +1,112 @@ +"""Tests for the Freedompro switch.""" +from datetime import timedelta +from unittest.mock import ANY, patch + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + +uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W" + + +async def test_switch_get_state(hass, init_integration): + """Test states of the switch.""" + init_integration + registry = er.async_get(hass) + + entity_id = "switch.irrigation_switch" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes.get("friendly_name") == "Irrigation switch" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + state_response["state"]["on"] = True + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "Irrigation switch" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_ON + + +async def test_switch_set_off(hass, init_integration): + """Test set off of the switch.""" + init_integration + registry = er.async_get(hass) + + entity_id = "switch.irrigation_switch" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "Irrigation switch" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch( + "homeassistant.components.freedompro.switch.put_state" + ) as mock_put_state: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": false}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_switch_set_on(hass, init_integration): + """Test set on of the switch.""" + init_integration + registry = er.async_get(hass) + + entity_id = "switch.irrigation_switch" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "Irrigation switch" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch( + "homeassistant.components.freedompro.switch.put_state" + ) as mock_put_state: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": true}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON From a021d7d6287e9177d08e32b37d93e21d93d640cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jul 2021 22:26:00 -1000 Subject: [PATCH 251/818] Expose async_get_source_ip in the network integration (#52901) * Expose async_get_source_ip in the network integration * Handle source ip on disabled interface * add coverage --- homeassistant/components/network/__init__.py | 14 +++++ tests/components/network/test_init.py | 64 ++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 3f19103acaa..6f11b0947d8 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import util from .const import ( ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, @@ -31,6 +32,19 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: return network.adapters +@bind_hass +async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str | None: + """Get the source ip for a target ip.""" + adapters = await async_get_adapters(hass) + all_ipv4s = [] + for adapter in adapters: + if adapter["enabled"] and (ipv4s := adapter["ipv4"]): + all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s]) + + source_ip = util.async_get_source_ip(target_ip) + return source_ip if source_ip in all_ipv4s else all_ipv4s[0] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up network for Home Assistant.""" diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index 41d87d5a805..bc4c543842f 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -7,6 +7,7 @@ from homeassistant.components import network from homeassistant.components.network.const import ( ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, + MDNS_TARGET_IP, STORAGE_KEY, STORAGE_VERSION, ) @@ -444,3 +445,66 @@ async def test_interfaces_configured_from_storage_websocket_update( "name": "vtun0", }, ] + + +async def test_async_get_source_ip_matching_interface(hass, hass_storage): + """Test getting the source ip address with interface matching.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=["192.168.1.5"], + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" + + +async def test_async_get_source_ip_interface_not_match(hass, hass_storage): + """Test getting the source ip address with interface does not match.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["vtun0"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=["192.168.1.5"], + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "169.254.3.2" + + +async def test_async_get_source_ip_cannot_determine_target(hass, hass_storage): + """Test getting the source ip address when getsockname fails.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[None], + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" From 9cbf88d9448240fa24c0974075439b5efe3b4be0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jul 2021 04:31:49 -0400 Subject: [PATCH 252/818] Switch to using entry.async_on_remove (#52952) --- homeassistant/components/zwave_js/__init__.py | 19 ++++++------------- homeassistant/components/zwave_js/api.py | 3 ++- .../components/zwave_js/binary_sensor.py | 4 ++-- homeassistant/components/zwave_js/climate.py | 4 ++-- homeassistant/components/zwave_js/const.py | 1 - homeassistant/components/zwave_js/cover.py | 4 ++-- homeassistant/components/zwave_js/fan.py | 4 ++-- homeassistant/components/zwave_js/light.py | 4 ++-- homeassistant/components/zwave_js/lock.py | 4 ++-- homeassistant/components/zwave_js/number.py | 4 ++-- homeassistant/components/zwave_js/sensor.py | 6 +++--- homeassistant/components/zwave_js/switch.py | 4 ++-- 12 files changed, 27 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index fee4da743c8..2bb25000504 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections import defaultdict -from typing import Callable from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient @@ -61,7 +60,6 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DATA_PLATFORM_SETUP, - DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, @@ -126,9 +124,7 @@ async def async_setup_entry( # noqa: C901 ent_reg = entity_registry.async_get(hass) entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) - unsubscribe_callbacks: list[Callable] = [] entry_hass_data[DATA_CLIENT] = client - entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks entry_hass_data[DATA_PLATFORM_SETUP] = {} registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) @@ -181,7 +177,7 @@ async def async_setup_entry( # noqa: C901 # add listener for value updated events if necessary if value_updates_disc_info: - unsubscribe_callbacks.append( + entry.async_on_unload( node.on( "value updated", lambda event: async_on_value_updated( @@ -191,14 +187,14 @@ async def async_setup_entry( # noqa: C901 ) # add listener for stateless node value notification events - unsubscribe_callbacks.append( + entry.async_on_unload( node.on( "value notification", lambda event: async_on_value_notification(event["value_notification"]), ) ) # add listener for stateless node notification events - unsubscribe_callbacks.append( + entry.async_on_unload( node.on( "notification", lambda event: async_on_notification(event["notification"]), @@ -400,7 +396,7 @@ async def async_setup_entry( # noqa: C901 client_listen(hass, entry, client, driver_ready) ) entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task - unsubscribe_callbacks.append( + entry.async_on_unload( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) ) @@ -442,7 +438,7 @@ async def async_setup_entry( # noqa: C901 ) # listen for new nodes being added to the mesh - unsubscribe_callbacks.append( + entry.async_on_unload( client.driver.controller.on( "node added", lambda event: hass.async_create_task( @@ -452,7 +448,7 @@ async def async_setup_entry( # noqa: C901 ) # listen for nodes being removed from the mesh # NOTE: This will not remove nodes that were removed when HA was not running - unsubscribe_callbacks.append( + entry.async_on_unload( client.driver.controller.on( "node removed", lambda event: async_on_node_removed(event["node"]) ) @@ -515,9 +511,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" info = hass.data[DOMAIN][entry.entry_id] - for unsub in info[DATA_UNSUBSCRIBE]: - unsub() - tasks = [] for platform, task in info[DATA_PLATFORM_SETUP].items(): if task.done(): diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1c303eea053..1e1dc2382fc 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -48,13 +48,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, - DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, ) from .helpers import async_enable_statistics, update_data_collection_preference from .services import BITMASK_SCHEMA +DATA_UNSUBSCRIBE = "unsubs" + # general API constants ID = "id" ENTRY_ID = "entry_id" diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 537f4f8e49e..71de7270d9a 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -249,7 +249,7 @@ async def async_setup_entry( async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{BINARY_SENSOR_DOMAIN}", diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 43363538500..1621e87cfab 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -59,7 +59,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import convert_temperature -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value @@ -121,7 +121,7 @@ async def async_setup_entry( async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{CLIMATE_DOMAIN}", diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ae390ce4581..2bbe35de664 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -13,7 +13,6 @@ CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" DOMAIN = "zwave_js" DATA_CLIENT = "client" -DATA_UNSUBSCRIBE = "unsubs" DATA_PLATFORM_SETUP = "platform_setup" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index e01f2871604..71033277388 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -23,7 +23,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -57,7 +57,7 @@ async def async_setup_entry( entities.append(ZWaveCover(config_entry, client, info)) async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{COVER_DOMAIN}", diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 89b99e90110..71f483c548f 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -21,7 +21,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -45,7 +45,7 @@ async def async_setup_entry( entities.append(ZwaveFan(config_entry, client, info)) async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{FAN_DOMAIN}", diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 748d199e93f..f3cabe8b6a7 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -27,7 +27,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -62,7 +62,7 @@ async def async_setup_entry( light = ZwaveLight(config_entry, client, info) async_add_entities([light]) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{LIGHT_DOMAIN}", diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 42230a9c267..ad4a736d63e 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -62,7 +62,7 @@ async def async_setup_entry( async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{LOCK_DOMAIN}", async_add_lock ) diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 2a3c9820a69..e53e5942999 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -29,7 +29,7 @@ async def async_setup_entry( entities.append(ZwaveNumberEntity(config_entry, client, info)) async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{NUMBER_DOMAIN}", diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 064275e5729..f20a12a519a 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -29,7 +29,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity from .helpers import get_device_id @@ -73,7 +73,7 @@ async def async_setup_entry( """Add node status sensor.""" async_add_entities([ZWaveNodeStatusSensor(config_entry, client, node)]) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{SENSOR_DOMAIN}", @@ -81,7 +81,7 @@ async def async_setup_entry( ) ) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_node_status_sensor", diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 1fb5480f2a1..ae98ddc563a 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -44,7 +44,7 @@ async def async_setup_entry( async_add_entities(entities) - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{SWITCH_DOMAIN}", From c5556a091e533a78c356aee8b834f8027754484a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Jul 2021 10:35:55 +0200 Subject: [PATCH 253/818] Return empty when listing statistic_ids for unsupported statistic (#52954) --- homeassistant/components/recorder/statistics.py | 4 +++- tests/components/history/test_init.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2ef49df7ded..4ddf380ee3a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -145,8 +145,10 @@ def _get_metadata(hass, session, statistic_ids, statistic_type): ) if statistic_type == "mean": baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False)) - if statistic_type == "sum": + elif statistic_type == "sum": baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False)) + elif statistic_type is not None: + return {} result = execute(baked_query(session).params(statistic_ids=statistic_ids)) metadata_ids = [metadata[0] for metadata in result] diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index c4f85717cac..df4a59a372c 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1015,9 +1015,7 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) ) response = await client.receive_json() assert response["success"] - assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": unit} - ] + assert response["result"] == [] await client.send_json( {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "mean"} From ac39607ae973dcd9b91fc03967c0aca721320f4b Mon Sep 17 00:00:00 2001 From: Lincoln Kirchoff Date: Tue, 13 Jul 2021 03:36:54 -0500 Subject: [PATCH 254/818] Fix modbus climate precision configuration variable (#52651) * Updated precision to follow the tenths, halves, whole notation used by other home assistant climate modules. Added the precision @property so that home assistant can handle this rounding in the frontend, rather than in the _async_read_register() method. * Fixed a pylinter error for periods in user-facing log messages, and updated `precision` defaults in components/modbus/__init__.py to be consistent with an error case, using `PRECISION_WHOLE`. * revert changes to `precision:` configuration variable instead, the climate `precision()` function will infer whether or not to display in whole or tenths. halves will be unsupported, which should be fine. * re-added missing line that was removed * revert change to use self._input_type instead of CALL_TYPE_REGISTER_HOLDING --- homeassistant/components/modbus/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index bfc04024e45..54894a4227c 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -16,6 +16,8 @@ from homeassistant.const import ( CONF_OFFSET, CONF_STRUCTURE, CONF_TEMPERATURE_UNIT, + PRECISION_TENTHS, + PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -134,6 +136,11 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): """Return the unit of measurement.""" return TEMP_FAHRENHEIT if self._unit == "F" else TEMP_CELSIUS + @property + def precision(self) -> float: + """Return the precision of the system.""" + return PRECISION_TENTHS if self._precision >= 1 else PRECISION_WHOLE + @property def min_temp(self): """Return the minimum temperature.""" From 30def802fc6a54881a2dba6886df655d9739bd97 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jul 2021 04:42:35 -0400 Subject: [PATCH 255/818] Validate tone is valid when processing siren.turn_on service call (#52953) * Validate tone is valid when processing siren.turn_on service call * Better message --- homeassistant/components/siren/__init__.py | 16 +++++++++++++--- tests/components/demo/test_siren.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index f571f87c93e..e5e03088a49 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -51,14 +51,24 @@ class SirenTurnOnServiceParameters(TypedDict, total=False): volume_level: float -def filter_turn_on_params( +def process_turn_on_params( siren: SirenEntity, params: SirenTurnOnServiceParameters ) -> SirenTurnOnServiceParameters: - """Filter out params not supported by the siren.""" + """ + Process turn_on service params. + + Filters out unsupported params and validates the rest. + """ supported_features = siren.supported_features or 0 if not supported_features & SUPPORT_TONES: params.pop(ATTR_TONE, None) + elif ATTR_TONE in params and ( + not siren.available_tones + or (tone := params[ATTR_TONE]) not in siren.available_tones + ): + raise ValueError(f"Tone {tone} is not a valid tone for this device") + if not supported_features & SUPPORT_DURATION: params.pop(ATTR_DURATION, None) if not supported_features & SUPPORT_VOLUME_SET: @@ -84,7 +94,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if k in (ATTR_TONE, ATTR_DURATION, ATTR_VOLUME_LEVEL) } await siren.async_turn_on( - **filter_turn_on_params(siren, cast(SirenTurnOnServiceParameters, data)) + **process_turn_on_params(siren, cast(SirenTurnOnServiceParameters, data)) ) component.async_register_entity_service( diff --git a/tests/components/demo/test_siren.py b/tests/components/demo/test_siren.py index 117343d2958..9af31b53c74 100644 --- a/tests/components/demo/test_siren.py +++ b/tests/components/demo/test_siren.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.siren.const import ( ATTR_AVAILABLE_TONES, + ATTR_TONE, ATTR_VOLUME_LEVEL, DOMAIN, ) @@ -56,6 +57,15 @@ async def test_turn_on(hass): state = hass.states.get(ENTITY_SIREN) assert state.state == STATE_ON + # Test that an invalid tone will raise a ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_SIREN_WITH_ALL_FEATURES, ATTR_TONE: "invalid_tone"}, + blocking=True, + ) + async def test_turn_off(hass): """Test turn off device.""" From 3408912dedb0ceb057cd4659a9d739a985070af8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Jul 2021 10:55:18 +0200 Subject: [PATCH 256/818] Improve docstring for async_get_device_class_lookup (#52921) --- homeassistant/helpers/entity_registry.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index fc9ef575c7d..69415030a87 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -160,8 +160,14 @@ class EntityRegistry: ) @callback - def async_get_device_class_lookup(self, domain_device_classes: set) -> dict: - """Return a lookup for the device class by domain.""" + def async_get_device_class_lookup( + self, domain_device_classes: set[tuple[str, str | None]] + ) -> dict: + """Return a lookup of entity ids for devices which have matching entities. + + Entities must match a set of (domain, device_class) tuples. + The result is indexed by device_id, then by the matching (domain, device_class) + """ lookup: dict[str, dict[tuple[Any, Any], str]] = {} for entity in self.entities.values(): if not entity.device_id: From e9948100a7cbc8020fb78bda48155529dafa74e1 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Tue, 13 Jul 2021 15:25:29 +0400 Subject: [PATCH 257/818] Add generic hygrostat integration (#36759) * generic_hygrostat: new integration Co-authored-by: J. Nick Koston Co-authored-by: jan Iversen --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/generic_hygrostat/__init__.py | 78 + .../generic_hygrostat/humidifier.py | 465 +++++ .../generic_hygrostat/manifest.json | 8 + .../generic_hygrostat/services.yaml | 0 .../components/generic_hygrostat/__init__.py | 1 + .../generic_hygrostat/test_humidifier.py | 1657 +++++++++++++++++ 8 files changed, 2211 insertions(+) create mode 100644 homeassistant/components/generic_hygrostat/__init__.py create mode 100644 homeassistant/components/generic_hygrostat/humidifier.py create mode 100644 homeassistant/components/generic_hygrostat/manifest.json create mode 100644 homeassistant/components/generic_hygrostat/services.yaml create mode 100644 tests/components/generic_hygrostat/__init__.py create mode 100644 tests/components/generic_hygrostat/test_humidifier.py diff --git a/.coveragerc b/.coveragerc index 8db2deee2e2..685642612fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -360,6 +360,7 @@ omit = homeassistant/components/garmin_connect/alarm_util.py homeassistant/components/gc100/* homeassistant/components/geniushub/* + homeassistant/components/generic_hygrostat/* homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ff7dcab2cb3..c269ad64888 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -177,6 +177,7 @@ homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/garmin_connect/* @cyberjunky homeassistant/components/gdacs/* @exxamalte +homeassistant/components/generic_hygrostat/* @Shulyaka homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_json_events/* @exxamalte homeassistant/components/geo_rss_events/* @exxamalte diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py new file mode 100644 index 00000000000..568863adb73 --- /dev/null +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -0,0 +1,78 @@ +"""The generic_hygrostat component.""" + +import logging + +import voluptuous as vol + +from homeassistant.components.humidifier.const import ( + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, +) +from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv, discovery + +DOMAIN = "generic_hygrostat" + +_LOGGER = logging.getLogger(__name__) + +CONF_HUMIDIFIER = "humidifier" +CONF_SENSOR = "target_sensor" +CONF_MIN_HUMIDITY = "min_humidity" +CONF_MAX_HUMIDITY = "max_humidity" +CONF_TARGET_HUMIDITY = "target_humidity" +CONF_DEVICE_CLASS = "device_class" +CONF_MIN_DUR = "min_cycle_duration" +CONF_DRY_TOLERANCE = "dry_tolerance" +CONF_WET_TOLERANCE = "wet_tolerance" +CONF_KEEP_ALIVE = "keep_alive" +CONF_INITIAL_STATE = "initial_state" +CONF_AWAY_HUMIDITY = "away_humidity" +CONF_AWAY_FIXED = "away_fixed" +CONF_STALE_DURATION = "sensor_stale_duration" + +DEFAULT_TOLERANCE = 3 +DEFAULT_NAME = "Generic Hygrostat" + +HYGROSTAT_SCHEMA = vol.Schema( + { + vol.Required(CONF_HUMIDIFIER): cv.entity_id, + vol.Required(CONF_SENSOR): cv.entity_id, + vol.Optional(CONF_DEVICE_CLASS): vol.In( + [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] + ), + vol.Optional(CONF_MAX_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MIN_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DRY_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), + vol.Optional(CONF_WET_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), + vol.Optional(CONF_TARGET_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_KEEP_ALIVE): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_INITIAL_STATE): cv.boolean, + vol.Optional(CONF_AWAY_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_AWAY_FIXED): cv.boolean, + vol.Optional(CONF_STALE_DURATION): vol.All( + cv.time_period, cv.positive_timedelta + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [HYGROSTAT_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Generic Hygrostat component.""" + if DOMAIN not in config: + return True + + for hygrostat_conf in config[DOMAIN]: + hass.async_create_task( + discovery.async_load_platform( + hass, "humidifier", DOMAIN, hygrostat_conf, config + ) + ) + + return True diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py new file mode 100644 index 00000000000..ee1c8f65d1a --- /dev/null +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -0,0 +1,465 @@ +"""Adds support for generic hygrostat units.""" +import asyncio +import logging + +from homeassistant.components.humidifier import PLATFORM_SCHEMA, HumidifierEntity +from homeassistant.components.humidifier.const import ( + ATTR_HUMIDITY, + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + MODE_AWAY, + MODE_NORMAL, + SUPPORT_MODES, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import DOMAIN as HA_DOMAIN, callback +from homeassistant.helpers import condition +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_time_interval, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from . import ( + CONF_AWAY_FIXED, + CONF_AWAY_HUMIDITY, + CONF_DEVICE_CLASS, + CONF_DRY_TOLERANCE, + CONF_HUMIDIFIER, + CONF_INITIAL_STATE, + CONF_KEEP_ALIVE, + CONF_MAX_HUMIDITY, + CONF_MIN_DUR, + CONF_MIN_HUMIDITY, + CONF_SENSOR, + CONF_STALE_DURATION, + CONF_TARGET_HUMIDITY, + CONF_WET_TOLERANCE, + HYGROSTAT_SCHEMA, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_SAVED_HUMIDITY = "saved_humidity" + +SUPPORT_FLAGS = 0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA.schema) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the generic hygrostat platform.""" + if discovery_info: + config = discovery_info + name = config[CONF_NAME] + switch_entity_id = config[CONF_HUMIDIFIER] + sensor_entity_id = config[CONF_SENSOR] + min_humidity = config.get(CONF_MIN_HUMIDITY) + max_humidity = config.get(CONF_MAX_HUMIDITY) + target_humidity = config.get(CONF_TARGET_HUMIDITY) + device_class = config.get(CONF_DEVICE_CLASS) + min_cycle_duration = config.get(CONF_MIN_DUR) + sensor_stale_duration = config.get(CONF_STALE_DURATION) + dry_tolerance = config[CONF_DRY_TOLERANCE] + wet_tolerance = config[CONF_WET_TOLERANCE] + keep_alive = config.get(CONF_KEEP_ALIVE) + initial_state = config.get(CONF_INITIAL_STATE) + away_humidity = config.get(CONF_AWAY_HUMIDITY) + away_fixed = config.get(CONF_AWAY_FIXED) + + async_add_entities( + [ + GenericHygrostat( + name, + switch_entity_id, + sensor_entity_id, + min_humidity, + max_humidity, + target_humidity, + device_class, + min_cycle_duration, + dry_tolerance, + wet_tolerance, + keep_alive, + initial_state, + away_humidity, + away_fixed, + sensor_stale_duration, + ) + ] + ) + + +class GenericHygrostat(HumidifierEntity, RestoreEntity): + """Representation of a Generic Hygrostat device.""" + + def __init__( + self, + name, + switch_entity_id, + sensor_entity_id, + min_humidity, + max_humidity, + target_humidity, + device_class, + min_cycle_duration, + dry_tolerance, + wet_tolerance, + keep_alive, + initial_state, + away_humidity, + away_fixed, + sensor_stale_duration, + ): + """Initialize the hygrostat.""" + self._name = name + self._switch_entity_id = switch_entity_id + self._sensor_entity_id = sensor_entity_id + self._device_class = device_class + self._min_cycle_duration = min_cycle_duration + self._dry_tolerance = dry_tolerance + self._wet_tolerance = wet_tolerance + self._keep_alive = keep_alive + self._state = initial_state + self._saved_target_humidity = away_humidity or target_humidity + self._active = False + self._cur_humidity = None + self._humidity_lock = asyncio.Lock() + self._min_humidity = min_humidity + self._max_humidity = max_humidity + self._target_humidity = target_humidity + self._support_flags = SUPPORT_FLAGS + if away_humidity: + self._support_flags = SUPPORT_FLAGS | SUPPORT_MODES + self._away_humidity = away_humidity + self._away_fixed = away_fixed + self._sensor_stale_duration = sensor_stale_duration + self._remove_stale_tracking = None + self._is_away = False + if not self._device_class: + self._device_class = DEVICE_CLASS_HUMIDIFIER + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + # Add listener + async_track_state_change( + self.hass, self._sensor_entity_id, self._async_sensor_changed + ) + async_track_state_change( + self.hass, self._switch_entity_id, self._async_switch_changed + ) + + if self._keep_alive: + async_track_time_interval(self.hass, self._async_operate, self._keep_alive) + + @callback + async def _async_startup(event): + """Init on startup.""" + sensor_state = self.hass.states.get(self._sensor_entity_id) + await self._async_sensor_changed(self._sensor_entity_id, None, sensor_state) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + + old_state = await self.async_get_last_state() + if old_state is not None: + if old_state.attributes.get(ATTR_MODE) == MODE_AWAY: + self._is_away = True + self._saved_target_humidity = self._target_humidity + self._target_humidity = self._away_humidity or self._target_humidity + if old_state.attributes.get(ATTR_HUMIDITY): + self._target_humidity = int(old_state.attributes[ATTR_HUMIDITY]) + if old_state.attributes.get(ATTR_SAVED_HUMIDITY): + self._saved_target_humidity = int( + old_state.attributes[ATTR_SAVED_HUMIDITY] + ) + if old_state.state: + self._state = old_state.state == STATE_ON + if self._target_humidity is None: + if self._device_class == DEVICE_CLASS_HUMIDIFIER: + self._target_humidity = self.min_humidity + else: + self._target_humidity = self.max_humidity + _LOGGER.warning( + "No previously saved humidity, setting to %s", self._target_humidity + ) + if self._state is None: + self._state = False + + await _async_startup(None) # init the sensor + + @property + def available(self): + """Return True if entity is available.""" + return self._active + + @property + def state_attributes(self): + """Return the optional state attributes.""" + data = super().state_attributes + + if self._saved_target_humidity: + data[ATTR_SAVED_HUMIDITY] = self._saved_target_humidity + + return data + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def name(self): + """Return the name of the hygrostat.""" + return self._name + + @property + def is_on(self): + """Return true if the hygrostat is on.""" + return self._state + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + return self._target_humidity + + @property + def mode(self): + """Return the current mode.""" + if self._away_humidity is None: + return None + if self._is_away: + return MODE_AWAY + return MODE_NORMAL + + @property + def available_modes(self): + """Return a list of available modes.""" + if self._away_humidity: + return [MODE_NORMAL, MODE_AWAY] + return None + + @property + def device_class(self): + """Return the device class of the humidifier.""" + return self._device_class + + async def async_turn_on(self, **kwargs): + """Turn hygrostat on.""" + if not self._active: + return + self._state = True + await self._async_operate(force=True) + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn hygrostat off.""" + if not self._active: + return + self._state = False + if self._is_device_active: + await self._async_device_turn_off() + await self.async_update_ha_state() + + async def async_set_humidity(self, humidity: int): + """Set new target humidity.""" + if humidity is None: + return + + if self._is_away and self._away_fixed: + self._saved_target_humidity = humidity + await self.async_update_ha_state() + return + + self._target_humidity = humidity + await self._async_operate(force=True) + await self.async_update_ha_state() + + @property + def min_humidity(self): + """Return the minimum humidity.""" + if self._min_humidity: + return self._min_humidity + + # get default humidity from super class + return super().min_humidity + + @property + def max_humidity(self): + """Return the maximum humidity.""" + if self._max_humidity: + return self._max_humidity + + # Get default humidity from super class + return super().max_humidity + + @callback + async def _async_sensor_changed(self, entity_id, old_state, new_state): + """Handle ambient humidity changes.""" + if new_state is None: + return + + if self._sensor_stale_duration: + if self._remove_stale_tracking: + self._remove_stale_tracking() + self._remove_stale_tracking = async_track_time_interval( + self.hass, + self._async_sensor_not_responding, + self._sensor_stale_duration, + ) + + await self._async_update_humidity(new_state.state) + await self._async_operate() + await self.async_update_ha_state() + + @callback + async def _async_sensor_not_responding(self, now=None): + """Handle sensor stale event.""" + + _LOGGER.debug( + "Sensor has not been updated for %s", + now - self.hass.states.get(self._sensor_entity_id).last_updated, + ) + _LOGGER.warning("Sensor is stalled, call the emergency stop") + await self._async_update_humidity("Stalled") + + @callback + def _async_switch_changed(self, entity_id, old_state, new_state): + """Handle humidifier switch state changes.""" + if new_state is None: + return + self.async_schedule_update_ha_state() + + async def _async_update_humidity(self, humidity): + """Update hygrostat with latest state from sensor.""" + try: + self._cur_humidity = float(humidity) + except ValueError as ex: + _LOGGER.warning("Unable to update from sensor: %s", ex) + self._cur_humidity = None + self._active = False + if self._is_device_active: + await self._async_device_turn_off() + + async def _async_operate(self, time=None, force=False): + """Check if we need to turn humidifying on or off.""" + async with self._humidity_lock: + if not self._active and None not in ( + self._cur_humidity, + self._target_humidity, + ): + self._active = True + force = True + _LOGGER.info( + "Obtained current and target humidity. " + "Generic hygrostat active. %s, %s", + self._cur_humidity, + self._target_humidity, + ) + + if not self._active or not self._state: + return + + if not force and time is None: + # If the `force` argument is True, we + # ignore `min_cycle_duration`. + # If the `time` argument is not none, we were invoked for + # keep-alive purposes, and `min_cycle_duration` is irrelevant. + if self._min_cycle_duration: + if self._is_device_active: + current_state = STATE_ON + else: + current_state = STATE_OFF + long_enough = condition.state( + self.hass, + self._switch_entity_id, + current_state, + self._min_cycle_duration, + ) + if not long_enough: + return + + if force: + # Ignore the tolerance when switched on manually + dry_tolerance = 0 + wet_tolerance = 0 + else: + dry_tolerance = self._dry_tolerance + wet_tolerance = self._wet_tolerance + + too_dry = self._target_humidity - self._cur_humidity >= dry_tolerance + too_wet = self._cur_humidity - self._target_humidity >= wet_tolerance + if self._is_device_active: + if (self._device_class == DEVICE_CLASS_HUMIDIFIER and too_wet) or ( + self._device_class == DEVICE_CLASS_DEHUMIDIFIER and too_dry + ): + _LOGGER.info("Turning off humidifier %s", self._switch_entity_id) + await self._async_device_turn_off() + elif time is not None: + # The time argument is passed only in keep-alive case + await self._async_device_turn_on() + else: + if (self._device_class == DEVICE_CLASS_HUMIDIFIER and too_dry) or ( + self._device_class == DEVICE_CLASS_DEHUMIDIFIER and too_wet + ): + _LOGGER.info("Turning on humidifier %s", self._switch_entity_id) + await self._async_device_turn_on() + elif time is not None: + # The time argument is passed only in keep-alive case + await self._async_device_turn_off() + + @property + def _is_device_active(self): + """If the toggleable device is currently active.""" + return self.hass.states.is_state(self._switch_entity_id, STATE_ON) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support_flags + + async def _async_device_turn_on(self): + """Turn humidifier toggleable device on.""" + data = {ATTR_ENTITY_ID: self._switch_entity_id} + await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data) + + async def _async_device_turn_off(self): + """Turn humidifier toggleable device off.""" + data = {ATTR_ENTITY_ID: self._switch_entity_id} + await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data) + + async def async_set_mode(self, mode: str): + """Set new mode. + + This method must be run in the event loop and returns a coroutine. + """ + if self._away_humidity is None: + return + if mode == MODE_AWAY and not self._is_away: + self._is_away = True + if not self._saved_target_humidity: + self._saved_target_humidity = self._away_humidity + self._saved_target_humidity, self._target_humidity = ( + self._target_humidity, + self._saved_target_humidity, + ) + await self._async_operate(force=True) + elif mode == MODE_NORMAL and self._is_away: + self._is_away = False + self._saved_target_humidity, self._target_humidity = ( + self._target_humidity, + self._saved_target_humidity, + ) + await self._async_operate(force=True) + + await self.async_update_ha_state() diff --git a/homeassistant/components/generic_hygrostat/manifest.json b/homeassistant/components/generic_hygrostat/manifest.json new file mode 100644 index 00000000000..5874097dc84 --- /dev/null +++ b/homeassistant/components/generic_hygrostat/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "generic_hygrostat", + "name": "Generic hygrostat", + "documentation": "https://www.home-assistant.io/integrations/generic_hygrostat", + "codeowners": ["@Shulyaka"], + "quality_scale": "internal", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/generic_hygrostat/services.yaml b/homeassistant/components/generic_hygrostat/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/generic_hygrostat/__init__.py b/tests/components/generic_hygrostat/__init__.py new file mode 100644 index 00000000000..6c3a131276c --- /dev/null +++ b/tests/components/generic_hygrostat/__init__.py @@ -0,0 +1 @@ +"""Tests for the generic_hygrostat component.""" diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py new file mode 100644 index 00000000000..d0412731c78 --- /dev/null +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -0,0 +1,1657 @@ +"""The tests for the generic_hygrostat.""" +import datetime +from unittest.mock import patch + +import pytest +import voluptuous as vol + +from homeassistant.components import input_boolean, switch +from homeassistant.components.humidifier.const import ( + ATTR_HUMIDITY, + DOMAIN, + MODE_AWAY, + MODE_NORMAL, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +import homeassistant.core as ha +from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + mock_restore_cache, +) + +ENTITY = "humidifier.test" +ENT_SENSOR = "sensor.test" +ENT_SWITCH = "switch.test" +ATTR_SAVED_HUMIDITY = "saved_humidity" +MIN_HUMIDITY = 20 +MAX_HUMIDITY = 65 +TARGET_HUMIDITY = 42 + + +async def test_setup_missing_conf(hass): + """Test set up humidity_control with missing config values.""" + config = { + "platform": "generic_hygrostat", + "name": "test", + "target_sensor": ENT_SENSOR, + } + with assert_setup_component(0): + await async_setup_component(hass, "humidifier", {"humidifier": config}) + await hass.async_block_till_done() + + +async def test_valid_conf(hass): + """Test set up generic_hygrostat with valid config values.""" + assert await async_setup_component( + hass, + "humidifier", + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + } + }, + ) + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_comp_1(hass): + """Initialize components.""" + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + +async def test_humidifier_input_boolean(hass, setup_comp_1): + """Test humidifier switching input_boolean.""" + humidifier_switch = "input_boolean.test" + assert await async_setup_component( + hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}} + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "humidifier": humidifier_switch, + "target_sensor": ENT_SENSOR, + "initial_state": True, + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get(humidifier_switch).state == STATE_OFF + + _setup_sensor(hass, 23) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 32}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get(humidifier_switch).state == STATE_ON + + +async def test_humidifier_switch(hass, setup_comp_1, enable_custom_integrations): + """Test humidifier switching test switch.""" + platform = getattr(hass.components, "test.switch") + platform.init() + switch_1 = platform.ENTITIES[1] + assert await async_setup_component( + hass, switch.DOMAIN, {"switch": {"platform": "test"}} + ) + await hass.async_block_till_done() + humidifier_switch = switch_1.entity_id + + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "humidifier": humidifier_switch, + "target_sensor": ENT_SENSOR, + "initial_state": True, + } + }, + ) + + await hass.async_block_till_done() + assert hass.states.get(humidifier_switch).state == STATE_OFF + + _setup_sensor(hass, 23) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 32}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get(humidifier_switch).state == STATE_ON + + +def _setup_sensor(hass, humidity): + """Set up the test sensor.""" + hass.states.async_set(ENT_SENSOR, humidity) + + +@pytest.fixture +async def setup_comp_0(hass): + """Initialize components.""" + _setup_sensor(hass, 45) + hass.states.async_set(ENT_SWITCH, STATE_OFF) + await hass.async_block_till_done() + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 2, + "wet_tolerance": 4, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + "away_humidity": 35, + "initial_state": True, + } + }, + ) + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_comp_2(hass): + """Initialize components.""" + _setup_sensor(hass, 45) + hass.states.async_set(ENT_SWITCH, STATE_OFF) + await hass.async_block_till_done() + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 2, + "wet_tolerance": 4, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 35, + "initial_state": True, + } + }, + ) + await hass.async_block_till_done() + + +async def test_unavailable_state(hass): + """Test the setting of defaults to unknown.""" + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 2, + "wet_tolerance": 4, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 35, + } + }, + ) + # The target sensor is unavailable, that should propagate to the humidifier entity: + await hass.async_block_till_done() + assert hass.states.get(ENTITY).state == STATE_UNAVAILABLE + + # Sensor online + _setup_sensor(hass, 30) + await hass.async_block_till_done() + assert hass.states.get(ENTITY).state == STATE_OFF + + +async def test_setup_defaults_to_unknown(hass): + """Test the setting of defaults to unknown.""" + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 2, + "wet_tolerance": 4, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 35, + } + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY).state == STATE_UNAVAILABLE + + +async def test_default_setup_params(hass, setup_comp_2): + """Test the setup with default parameters.""" + state = hass.states.get(ENTITY) + assert state.attributes.get("min_humidity") == 0 + assert state.attributes.get("max_humidity") == 100 + assert state.attributes.get("humidity") == 0 + + +async def test_default_setup_params_dehumidifier(hass, setup_comp_0): + """Test the setup with default parameters for dehumidifier.""" + state = hass.states.get(ENTITY) + assert state.attributes.get("min_humidity") == 0 + assert state.attributes.get("max_humidity") == 100 + assert state.attributes.get("humidity") == 100 + + +async def test_get_modes(hass, setup_comp_2): + """Test that the attributes returns the correct modes.""" + state = hass.states.get(ENTITY) + modes = state.attributes.get("available_modes") + assert modes == [MODE_NORMAL, MODE_AWAY] + + +async def test_set_target_humidity(hass, setup_comp_2): + """Test the setting of the target humidity.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 40}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 40 + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: None}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 40 + + +async def test_set_away_mode(hass, setup_comp_2): + """Test the setting away mode.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 44}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 35 + + +async def test_set_away_mode_and_restore_prev_humidity(hass, setup_comp_2): + """Test the setting and removing away mode. + + Verify original humidity is restored. + """ + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 44}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 35 + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_NORMAL}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 44 + + +async def test_set_away_mode_twice_and_restore_prev_humidity(hass, setup_comp_2): + """Test the setting away mode twice in a row. + + Verify original humidity is restored. + """ + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 44}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 35 + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_NORMAL}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 44 + + +async def test_sensor_bad_value(hass, setup_comp_2): + """Test sensor that have None as state.""" + state = hass.states.get(ENTITY) + humidity = state.attributes.get("current_humidity") + + _setup_sensor(hass, None) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY) + assert humidity == state.attributes.get("current_humidity") + + +async def test_set_target_humidity_humidifier_on(hass, setup_comp_2): + """Test if target humidity turn humidifier on.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 36) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 45}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_set_target_humidity_humidifier_off(hass, setup_comp_2): + """Test if target humidity turn humidifier off.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 36}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_humidifier_on_within_tolerance(hass, setup_comp_2): + """Test if humidity change doesn't turn on within tolerance.""" + calls = await _setup_switch(hass, False) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 44}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 43) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_humidifier_on_outside_tolerance(hass, setup_comp_2): + """Test if humidity change turn humidifier on outside dry tolerance.""" + calls = await _setup_switch(hass, False) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 44}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 42) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_humidifier_off_within_tolerance(hass, setup_comp_2): + """Test if humidity change doesn't turn off within tolerance.""" + calls = await _setup_switch(hass, True) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 46}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 48) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_humidifier_off_outside_tolerance(hass, setup_comp_2): + """Test if humidity change turn humidifier off outside wet tolerance.""" + calls = await _setup_switch(hass, True) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 46}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 50) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_operation_mode_humidify(hass, setup_comp_2): + """Test change mode from OFF to HUMIDIFY. + + Switch turns on when humidity below setpoint and mode changes. + """ + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 45}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 40) + await hass.async_block_till_done() + calls = await _setup_switch(hass, False) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def _setup_switch(hass, is_on): + """Set up the test switch.""" + hass.states.async_set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) + calls = [] + + @callback + def log_call(call): + """Log service calls.""" + calls.append(call) + + hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, log_call) + hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, log_call) + + await hass.async_block_till_done() + return calls + + +@pytest.fixture +async def setup_comp_3(hass): + """Initialize components.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 2, + "wet_tolerance": 4, + "away_humidity": 30, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + + +async def test_set_target_humidity_dry_off(hass, setup_comp_3): + """Test if target humidity turn dry off.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 50) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 55}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_turn_away_mode_on_drying(hass, setup_comp_3): + """Test the setting away mode when drying.""" + await _setup_switch(hass, True) + _setup_sensor(hass, 50) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 34}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY, ATTR_MODE: MODE_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("humidity") == 30 + + +async def test_operation_mode_dry(hass, setup_comp_3): + """Test change mode from OFF to DRY. + + Switch turns on when humidity below setpoint and state changes. + """ + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 30) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_set_target_humidity_dry_on(hass, setup_comp_3): + """Test if target humidity turn dry on.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_init_ignores_tolerance(hass, setup_comp_3): + """Test if tolerance is ignored on initialization.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 39) + await hass.async_block_till_done() + assert 1 == len(calls) + call = calls[0] + assert HASS_DOMAIN == call.domain + assert SERVICE_TURN_OFF == call.service + assert ENT_SWITCH == call.data["entity_id"] + + +async def test_humidity_change_dry_off_within_tolerance(hass, setup_comp_3): + """Test if humidity change doesn't turn dry off within tolerance.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + _setup_sensor(hass, 39) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_set_humidity_change_dry_off_outside_tolerance(hass, setup_comp_3): + """Test if humidity change turn dry off.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 36) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_dry_on_within_tolerance(hass, setup_comp_3): + """Test if humidity change doesn't turn dry on within tolerance.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 37) + _setup_sensor(hass, 41) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_dry_on_outside_tolerance(hass, setup_comp_3): + """Test if humidity change turn dry on.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_running_when_operating_mode_is_off_2(hass, setup_comp_3): + """Test that the switch turns off when enabled is set False.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3): + """Test that the switch doesn't turn on when enabled is False.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 30) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + +@pytest.fixture +async def setup_comp_4(hass): + """Initialize components.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 3, + "wet_tolerance": 3, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + "min_cycle_duration": datetime.timedelta(minutes=10), + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + + +async def test_humidity_change_dry_trigger_on_not_long_enough(hass, setup_comp_4): + """Test if humidity change turn dry on.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_dry_trigger_on_long_enough(hass, setup_comp_4): + """Test if humidity change turn dry on.""" + fake_changed = datetime.datetime( + 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + ) + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed + ): + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_dry_trigger_off_not_long_enough(hass, setup_comp_4): + """Test if humidity change turn dry on.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_dry_trigger_off_long_enough(hass, setup_comp_4): + """Test if humidity change turn dry on.""" + fake_changed = datetime.datetime( + 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + ) + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed + ): + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_mode_change_dry_trigger_off_not_long_enough(hass, setup_comp_4): + """Test if mode change turns dry off despite minimum cycle.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == "homeassistant" + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_mode_change_dry_trigger_on_not_long_enough(hass, setup_comp_4): + """Test if mode change turns dry on despite minimum cycle.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == "homeassistant" + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +@pytest.fixture +async def setup_comp_6(hass): + """Initialize components.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 3, + "wet_tolerance": 3, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "min_cycle_duration": datetime.timedelta(minutes=10), + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + + +async def test_humidity_change_humidifier_trigger_off_not_long_enough( + hass, setup_comp_6 +): + """Test if humidity change doesn't turn humidifier off because of time.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_humidifier_trigger_on_not_long_enough( + hass, setup_comp_6 +): + """Test if humidity change doesn't turn humidifier on because of time.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_humidity_change_humidifier_trigger_on_long_enough(hass, setup_comp_6): + """Test if humidity change turn humidifier on after min cycle.""" + fake_changed = datetime.datetime( + 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + ) + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed + ): + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_humidifier_trigger_off_long_enough(hass, setup_comp_6): + """Test if humidity change turn humidifier off after min cycle.""" + fake_changed = datetime.datetime( + 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + ) + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed + ): + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_mode_change_humidifier_trigger_off_not_long_enough(hass, setup_comp_6): + """Test if mode change turns humidifier off despite minimum cycle.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == "homeassistant" + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_mode_change_humidifier_trigger_on_not_long_enough(hass, setup_comp_6): + """Test if mode change turns humidifier on despite minimum cycle.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 0 + + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == "homeassistant" + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +@pytest.fixture +async def setup_comp_7(hass): + """Initialize components.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 3, + "wet_tolerance": 3, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + "min_cycle_duration": datetime.timedelta(minutes=15), + "keep_alive": datetime.timedelta(minutes=10), + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + + +async def test_humidity_change_dry_trigger_on_long_enough_3(hass, setup_comp_7): + """Test if turn on signal is sent at keep-alive intervals.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=5)) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_dry_trigger_off_long_enough_3(hass, setup_comp_7): + """Test if turn on signal is sent at keep-alive intervals.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=5)) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +@pytest.fixture +async def setup_comp_8(hass): + """Initialize components.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 3, + "wet_tolerance": 3, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "min_cycle_duration": datetime.timedelta(minutes=15), + "keep_alive": datetime.timedelta(minutes=10), + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + + +async def test_humidity_change_humidifier_trigger_on_long_enough_2(hass, setup_comp_8): + """Test if turn on signal is sent at keep-alive intervals.""" + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 35) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=5)) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_humidity_change_humidifier_trigger_off_long_enough_2(hass, setup_comp_8): + """Test if turn on signal is sent at keep-alive intervals.""" + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 45) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=5)) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_float_tolerance_values(hass): + """Test if dehumidifier does not turn on within floating point tolerance.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 0.2, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 45) + _setup_sensor(hass, 39.9) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_float_tolerance_values_2(hass): + """Test if dehumidifier turns off when oudside of floating point tolerance values.""" + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 0.2, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + "initial_state": True, + "target_humidity": 40, + } + }, + ) + await hass.async_block_till_done() + calls = await _setup_switch(hass, True) + _setup_sensor(hass, 39.7) + await hass.async_block_till_done() + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == ENT_SWITCH + + +async def test_custom_setup_params(hass): + """Test the setup with custom parameters.""" + _setup_sensor(hass, 45) + await hass.async_block_till_done() + result = await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "min_humidity": MIN_HUMIDITY, + "max_humidity": MAX_HUMIDITY, + "target_humidity": TARGET_HUMIDITY, + } + }, + ) + await hass.async_block_till_done() + assert result + state = hass.states.get(ENTITY) + assert state.attributes.get("min_humidity") == MIN_HUMIDITY + assert state.attributes.get("max_humidity") == MAX_HUMIDITY + assert state.attributes.get("humidity") == TARGET_HUMIDITY + + +async def test_restore_state(hass): + """Ensure states are restored on startup.""" + _setup_sensor(hass, 45) + await hass.async_block_till_done() + mock_restore_cache( + hass, + ( + State( + "humidifier.test_hygrostat", + STATE_OFF, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: "40", ATTR_MODE: MODE_AWAY}, + ), + ), + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test_hygrostat", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 32, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.attributes[ATTR_MODE] == MODE_AWAY + assert state.state == STATE_OFF + + +async def test_restore_state_target_humidity(hass): + """Ensure restore target humidity if available.""" + _setup_sensor(hass, 45) + await hass.async_block_till_done() + mock_restore_cache( + hass, + ( + State( + "humidifier.test_hygrostat", + STATE_OFF, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: "40"}, + ), + ), + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test_hygrostat", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 32, + "target_humidity": 50, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.state == STATE_OFF + + +async def test_restore_state_and_return_to_normal(hass): + """Ensure retain of target humidity for normal mode.""" + _setup_sensor(hass, 55) + await hass.async_block_till_done() + mock_restore_cache( + hass, + ( + State( + "humidifier.test_hygrostat", + STATE_OFF, + { + ATTR_ENTITY_ID: ENTITY, + ATTR_HUMIDITY: "40", + ATTR_MODE: MODE_AWAY, + ATTR_SAVED_HUMIDITY: "50", + }, + ), + ), + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test_hygrostat", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 32, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.attributes[ATTR_SAVED_HUMIDITY] == 50 + assert state.attributes[ATTR_MODE] == MODE_AWAY + assert state.state == STATE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: "humidifier.test_hygrostat", ATTR_MODE: MODE_NORMAL}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 50 + assert state.attributes[ATTR_MODE] == MODE_NORMAL + assert state.state == STATE_OFF + + +async def test_no_restore_state(hass): + """Ensure states are restored on startup if they exist. + + Allows for graceful reboot. + """ + _setup_sensor(hass, 45) + await hass.async_block_till_done() + mock_restore_cache( + hass, + ( + State( + "humidifier.test_hygrostat", + STATE_OFF, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: "40", ATTR_MODE: MODE_AWAY}, + ), + ), + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test_hygrostat", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "target_humidity": 42, + "away_humidity": 35, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.state == STATE_OFF + + +async def test_restore_state_uncoherence_case(hass): + """ + Test restore from a strange state. + + - Turn the generic hygrostat off + - Restart HA and restore state from DB + """ + _mock_restore_cache(hass, humidity=40) + + calls = await _setup_switch(hass, False) + _setup_sensor(hass, 35) + await _setup_humidifier(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY) + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.state == STATE_OFF + assert len(calls) == 0 + + calls = await _setup_switch(hass, False) + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.state == STATE_OFF + + +async def _setup_humidifier(hass): + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "dry_tolerance": 2, + "wet_tolerance": 4, + "away_humidity": 32, + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "device_class": "dehumidifier", + } + }, + ) + await hass.async_block_till_done() + + +def _mock_restore_cache(hass, humidity=40, state=STATE_OFF): + mock_restore_cache( + hass, + ( + State( + ENTITY, + state, + { + ATTR_ENTITY_ID: ENTITY, + ATTR_HUMIDITY: str(humidity), + ATTR_MODE: MODE_AWAY, + }, + ), + ), + ) + + +async def test_away_fixed_humidity_mode(hass): + """Ensure retain of target humidity for normal mode.""" + _setup_sensor(hass, 45) + await hass.async_block_till_done() + await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test_hygrostat", + "humidifier": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_humidity": 32, + "target_humidity": 40, + "away_fixed": True, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.attributes[ATTR_MODE] == MODE_NORMAL + assert state.state == STATE_OFF + + # Switch to Away mode + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: "humidifier.test_hygrostat", ATTR_MODE: MODE_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + + # Target humidity changed to away_humidity + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_MODE] == MODE_AWAY + assert state.attributes[ATTR_HUMIDITY] == 32 + assert state.attributes[ATTR_SAVED_HUMIDITY] == 40 + assert state.state == STATE_OFF + + # Change target humidity + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: "humidifier.test_hygrostat", ATTR_HUMIDITY: 42}, + blocking=True, + ) + await hass.async_block_till_done() + + # Current target humidity not changed + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 32 + assert state.attributes[ATTR_SAVED_HUMIDITY] == 42 + assert state.attributes[ATTR_MODE] == MODE_AWAY + assert state.state == STATE_OFF + + # Return to Normal mode + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: "humidifier.test_hygrostat", ATTR_MODE: MODE_NORMAL}, + blocking=True, + ) + await hass.async_block_till_done() + + # Target humidity changed to away_humidity + state = hass.states.get("humidifier.test_hygrostat") + assert state.attributes[ATTR_HUMIDITY] == 42 + assert state.attributes[ATTR_SAVED_HUMIDITY] == 32 + assert state.attributes[ATTR_MODE] == MODE_NORMAL + assert state.state == STATE_OFF + + +async def test_sensor_stale_duration(hass, setup_comp_1, caplog): + """Test turn off on sensor stale.""" + + humidifier_switch = "input_boolean.test" + assert await async_setup_component( + hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}} + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + DOMAIN, + { + "humidifier": { + "platform": "generic_hygrostat", + "name": "test", + "humidifier": humidifier_switch, + "target_sensor": ENT_SENSOR, + "initial_state": True, + "sensor_stale_duration": {"minutes": 10}, + } + }, + ) + await hass.async_block_till_done() + + _setup_sensor(hass, 23) + await hass.async_block_till_done() + + assert hass.states.get(humidifier_switch).state == STATE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY, ATTR_HUMIDITY: 32}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get(humidifier_switch).state == STATE_ON + + # Wait 11 minutes + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=11)) + await hass.async_block_till_done() + + # 11 minutes later, no news from the sensor : emergency cut off + assert hass.states.get(humidifier_switch).state == STATE_OFF + assert "emergency" in caplog.text + + # Updated value from sensor received + _setup_sensor(hass, 24) + await hass.async_block_till_done() + + # A new value has arrived, the humidifier should go ON + assert hass.states.get(humidifier_switch).state == STATE_ON + + # Manual turn off + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(humidifier_switch).state == STATE_OFF + + # Wait another 11 minutes + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=22)) + await hass.async_block_till_done() + + # Still off + assert hass.states.get(humidifier_switch).state == STATE_OFF + + # Updated value from sensor received + _setup_sensor(hass, 22) + await hass.async_block_till_done() + + # Not turning on by itself + assert hass.states.get(humidifier_switch).state == STATE_OFF From 96f6e0e4a4fd76afa876f1d8145e346208324463 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Tue, 13 Jul 2021 13:59:34 +0200 Subject: [PATCH 258/818] Add sensor support to Freedompro (#52726) * Update Freedompro * Update tests/components/freedompro/test_sensor.py Co-authored-by: Erik Montnemery * add new test for sensor and add unit_of measurement and state_class * add test state updates from the API * optimizer code test * optimizer code sensor * Update homeassistant/components/freedompro/sensor.py * Fix imports * Update homeassistant/components/freedompro/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/freedompro/sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/freedompro/sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- .../components/freedompro/__init__.py | 2 +- homeassistant/components/freedompro/sensor.py | 95 +++++++++++++++++++ tests/components/freedompro/const.py | 2 +- tests/components/freedompro/test_sensor.py | 76 +++++++++++++++ 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/freedompro/sensor.py create mode 100644 tests/components/freedompro/test_sensor.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 6e5dd438e2d..ddb6688a127 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["light", "switch"] +PLATFORMS = ["light", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py new file mode 100644 index 00000000000..00dbb30570c --- /dev/null +++ b/homeassistant/components/freedompro/sensor.py @@ -0,0 +1,95 @@ +"""Support for Freedompro sensor.""" +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) +from homeassistant.const import CONF_API_KEY, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +DEVICE_CLASS_MAP = { + "temperatureSensor": DEVICE_CLASS_TEMPERATURE, + "humiditySensor": DEVICE_CLASS_HUMIDITY, + "lightSensor": DEVICE_CLASS_ILLUMINANCE, +} +STATE_CLASS_MAP = { + "temperatureSensor": STATE_CLASS_MEASUREMENT, + "humiditySensor": STATE_CLASS_MEASUREMENT, + "lightSensor": None, +} +UNIT_MAP = { + "temperatureSensor": TEMP_CELSIUS, + "humiditySensor": PERCENTAGE, + "lightSensor": LIGHT_LUX, +} +DEVICE_KEY_MAP = { + "temperatureSensor": "currentTemperature", + "humiditySensor": "currentRelativeHumidity", + "lightSensor": "currentAmbientLightLevel", +} +SUPPORTED_SENSORS = {"temperatureSensor", "humiditySensor", "lightSensor"} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro sensor.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] in SUPPORTED_SENSORS + ) + + +class Device(CoordinatorEntity, SensorEntity): + """Representation of an Freedompro sensor.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro sensor.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": self._type, + "manufacturer": "Freedompro", + } + self._attr_device_class = DEVICE_CLASS_MAP[self._type] + self._attr_state_class = STATE_CLASS_MAP[self._type] + self._attr_unit_of_measurement = UNIT_MAP[self._type] + self._attr_state = 0 + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + self._attr_state = state[DEVICE_KEY_MAP[self._type]] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py index 0e8f5c4aa52..0db9d5884b1 100644 --- a/tests/components/freedompro/const.py +++ b/tests/components/freedompro/const.py @@ -207,7 +207,7 @@ DEVICES_STATE = [ { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM", "type": "lightSensor", - "state": {"currentAmbientLightLevel": 500}, + "state": {"currentAmbientLightLevel": 0}, "online": True, }, { diff --git a/tests/components/freedompro/test_sensor.py b/tests/components/freedompro/test_sensor.py new file mode 100644 index 00000000000..b6f809569f1 --- /dev/null +++ b/tests/components/freedompro/test_sensor.py @@ -0,0 +1,76 @@ +"""Tests for the Freedompro sensor.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + + +@pytest.mark.parametrize( + "entity_id, uid, name", + [ + ( + "sensor.garden_humidity_sensor", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY", + "Garden humidity sensor", + ), + ( + "sensor.living_room_temperature_sensor", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA", + "Living room temperature sensor", + ), + ( + "sensor.garden_light_sensors", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM", + "Garden light sensors", + ), + ], +) +async def test_sensor_get_state( + hass, init_integration, entity_id: str, uid: str, name: str +): + """Test states of the sensor.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == "0" + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + if state_response["type"] == "lightSensor": + state_response["state"]["currentAmbientLightLevel"] = "1" + if state_response["type"] == "temperatureSensor": + state_response["state"]["currentTemperature"] = "1" + if state_response["type"] == "humiditySensor": + state_response["state"]["currentRelativeHumidity"] = "1" + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == "1" From e563dc0d7b868b8c43e758dea576d6978f094eeb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Jul 2021 14:20:47 +0200 Subject: [PATCH 259/818] Set device_class on additional temperature sensors (#52960) * Set device_class on additional temperature sensors * Apply suggestions from code review Co-authored-by: Tobias Sauerwein * Set device class for greeneye_monitor sensor * Set device class for bme280 and bme680 sensor Co-authored-by: Tobias Sauerwein --- homeassistant/components/bme280/sensor.py | 10 +++++-- homeassistant/components/bme680/sensor.py | 14 +++++---- homeassistant/components/dht/sensor.py | 7 +++-- homeassistant/components/ecobee/sensor.py | 5 ++-- .../components/greeneye_monitor/sensor.py | 2 ++ homeassistant/components/htu21d/sensor.py | 14 ++++++++- homeassistant/components/mhz19/sensor.py | 7 +++-- homeassistant/components/mysensors/sensor.py | 6 ++-- homeassistant/components/temper/sensor.py | 2 ++ .../components/thermoworks_smoke/sensor.py | 2 ++ .../components/waterfurnace/sensor.py | 30 +++++++++++++++---- 11 files changed, 74 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 2c3ab0303b0..6a54432d190 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -12,6 +12,9 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_FAHRENHEIT, ) @@ -48,9 +51,9 @@ SENSOR_TEMP = "temperature" SENSOR_HUMID = "humidity" SENSOR_PRESS = "pressure" SENSOR_TYPES = { - SENSOR_TEMP: ["Temperature", None], - SENSOR_HUMID: ["Humidity", PERCENTAGE], - SENSOR_PRESS: ["Pressure", "mb"], + SENSOR_TEMP: ["Temperature", None, DEVICE_CLASS_TEMPERATURE], + SENSOR_HUMID: ["Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY], + SENSOR_PRESS: ["Pressure", "mb", DEVICE_CLASS_PRESSURE], } DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] @@ -146,6 +149,7 @@ class BME280Sensor(SensorEntity): self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property def name(self): diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index f3d6b9428ea..6e4e2de79b9 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -11,6 +11,9 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_FAHRENHEIT, ) @@ -53,11 +56,11 @@ SENSOR_PRESS = "pressure" SENSOR_GAS = "gas" SENSOR_AQ = "airquality" SENSOR_TYPES = { - SENSOR_TEMP: ["Temperature", None], - SENSOR_HUMID: ["Humidity", PERCENTAGE], - SENSOR_PRESS: ["Pressure", "mb"], - SENSOR_GAS: ["Gas Resistance", "Ohms"], - SENSOR_AQ: ["Air Quality", PERCENTAGE], + SENSOR_TEMP: ["Temperature", None, DEVICE_CLASS_TEMPERATURE], + SENSOR_HUMID: ["Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY], + SENSOR_PRESS: ["Pressure", "mb", DEVICE_CLASS_PRESSURE], + SENSOR_GAS: ["Gas Resistance", "Ohms", None], + SENSOR_AQ: ["Air Quality", PERCENTAGE, None], } DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS, SENSOR_AQ] OVERSAMPLING_VALUES = {0, 1, 2, 4, 8, 16} @@ -327,6 +330,7 @@ class BME680Sensor(SensorEntity): self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property def name(self): diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 72780832960..1300c165b37 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -12,6 +12,8 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PIN, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_FAHRENHEIT, ) @@ -33,8 +35,8 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) SENSOR_TEMPERATURE = "temperature" SENSOR_HUMIDITY = "humidity" SENSOR_TYPES = { - SENSOR_TEMPERATURE: ["Temperature", None], - SENSOR_HUMIDITY: ["Humidity", PERCENTAGE], + SENSOR_TEMPERATURE: ["Temperature", None, DEVICE_CLASS_TEMPERATURE], + SENSOR_HUMIDITY: ["Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY], } @@ -124,6 +126,7 @@ class DHTSensor(SensorEntity): self.humidity_offset = humidity_offset self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property def name(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 275db46ab0a..24ba36bedd8 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -12,8 +12,8 @@ from homeassistant.const import ( from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SENSOR_TYPES = { - "temperature": ["Temperature", TEMP_FAHRENHEIT], - "humidity": ["Humidity", PERCENTAGE], + "temperature": ["Temperature", TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE], + "humidity": ["Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY], } @@ -44,6 +44,7 @@ class EcobeeSensor(SensorEntity): self.index = sensor_index self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property def name(self): diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 337a471eb2b..0aa106e6801 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -4,6 +4,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SENSOR_TYPE, CONF_TEMPERATURE_UNIT, + DEVICE_CLASS_TEMPERATURE, POWER_WATT, TIME_HOURS, TIME_MINUTES, @@ -240,6 +241,7 @@ class PulseCounter(GEMSensor): class TemperatureSensor(GEMSensor): """Entity showing temperature from one temperature sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_icon = TEMPERATURE_ICON def __init__(self, monitor_serial_number, number, name, unit): diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index d32eebf6d5f..ccbe6a31de2 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -8,7 +8,13 @@ import smbus import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -32,6 +38,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +DEVICE_CLASS_MAP = { + SENSOR_TEMPERATURE: DEVICE_CLASS_TEMPERATURE, + SENSOR_HUMIDITY: DEVICE_CLASS_HUMIDITY, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HTU21D sensor.""" @@ -79,6 +90,7 @@ class HTU21DSensor(SensorEntity): self._unit_of_measurement = unit self._client = htu21d_client self._state = None + self._attr_device_class = DEVICE_CLASS_MAP[variable] @property def name(self) -> str: diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 0f0735dd5da..63a1181f720 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -11,6 +11,8 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, CONF_NAME, + DEVICE_CLASS_CO2, + DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv @@ -29,8 +31,8 @@ ATTR_CO2_CONCENTRATION = "co2_concentration" SENSOR_TEMPERATURE = "temperature" SENSOR_CO2 = "co2" SENSOR_TYPES = { - SENSOR_TEMPERATURE: ["Temperature", None], - SENSOR_CO2: ["CO2", CONCENTRATION_PARTS_PER_MILLION], + SENSOR_TEMPERATURE: ["Temperature", None, DEVICE_CLASS_TEMPERATURE], + SENSOR_CO2: ["CO2", CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -80,6 +82,7 @@ class MHZ19Sensor(SensorEntity): self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._ppm = None self._temperature = None + self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property def name(self): diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 6a5c80e1e8a..1cb2b632362 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -115,14 +115,12 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): @property def device_class(self) -> str | None: """Return the device class of this entity.""" - icon = self._get_sensor_type()[2] - return icon + return self._get_sensor_type()[2] @property def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" - icon = self._get_sensor_type()[1] - return icon + return self._get_sensor_type()[1] @property def unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 7edbd3ba812..7d447d3f9ea 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_OFFSET, + DEVICE_CLASS_TEMPERATURE, DEVICE_DEFAULT_NAME, TEMP_FAHRENHEIT, ) @@ -68,6 +69,7 @@ class TemperSensor(SensorEntity): self.current_value = None self._name = name self.set_temper_device(temper_device) + self._attr_device_class = DEVICE_CLASS_TEMPERATURE @property def name(self): diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 86427349f31..1bdbbc5fcc3 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_EXCLUDE, CONF_MONITORED_CONDITIONS, CONF_PASSWORD, + DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv @@ -105,6 +106,7 @@ class ThermoworksSmokeSensor(SensorEntity): self._unique_id = f"{serial}-{sensor_type}" self.serial = serial self.mgr = mgr + self._attr_device_class = DEVICE_CLASS_TEMPERATURE self.update_unit() @property diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index fe7c94ed634..8691cc4ed02 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -1,7 +1,12 @@ """Support for Waterfurnace.""" from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity -from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + POWER_WATT, + TEMP_FAHRENHEIT, +) from homeassistant.core import callback from homeassistant.util import slugify @@ -12,9 +17,15 @@ class WFSensorConfig: """Water Furnace Sensor configuration.""" def __init__( - self, friendly_name, field, icon="mdi:gauge", unit_of_measurement=None + self, + friendly_name, + field, + icon="mdi:gauge", + unit_of_measurement=None, + device_class=None, ): """Initialize configuration.""" + self.device_class = device_class self.friendly_name = friendly_name self.field = field self.icon = icon @@ -25,13 +36,19 @@ SENSORS = [ WFSensorConfig("Furnace Mode", "mode"), WFSensorConfig("Total Power", "totalunitpower", "mdi:flash", POWER_WATT), WFSensorConfig( - "Active Setpoint", "tstatactivesetpoint", "mdi:thermometer", TEMP_FAHRENHEIT + "Active Setpoint", + "tstatactivesetpoint", + None, + TEMP_FAHRENHEIT, + DEVICE_CLASS_TEMPERATURE, ), - WFSensorConfig("Leaving Air", "leavingairtemp", "mdi:thermometer", TEMP_FAHRENHEIT), - WFSensorConfig("Room Temp", "tstatroomtemp", "mdi:thermometer", TEMP_FAHRENHEIT), WFSensorConfig( - "Loop Temp", "enteringwatertemp", "mdi:thermometer", TEMP_FAHRENHEIT + "Leaving Air", "leavingairtemp", None, TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE ), + WFSensorConfig( + "Room Temp", "tstatroomtemp", None, TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE + ), + WFSensorConfig("Loop Temp", "enteringwatertemp", None, TEMP_FAHRENHEIT), WFSensorConfig( "Humidity Set Point", "tstathumidsetpoint", "mdi:water-percent", PERCENTAGE ), @@ -71,6 +88,7 @@ class WaterFurnaceSensor(SensorEntity): self._state = None self._icon = config.icon self._unit_of_measurement = config.unit_of_measurement + self._attr_device_class = config.device_class # This ensures that the sensors are isolated per waterfurnace unit self.entity_id = ENTITY_ID_FORMAT.format( From 55b0d562cebc8e86898895e746b1a4bf78617b96 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 13 Jul 2021 09:01:43 -0400 Subject: [PATCH 260/818] Use entity class attributes for automation (#52694) * Use entity class attributes for automation * tweak --- .../components/automation/__init__.py | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 1733f272229..b94029db4ee 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -272,6 +272,8 @@ async def async_setup(hass, config): class AutomationEntity(ToggleEntity, RestoreEntity): """Entity to show status of entity.""" + _attr_should_poll = False + def __init__( self, automation_id, @@ -287,8 +289,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trace_config, ): """Initialize an automation entity.""" - self._id = automation_id - self._name = name + self._attr_name = name self._trigger_config = trigger_config self._async_detach_triggers = None self._cond_func = cond_func @@ -304,21 +305,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._raw_config = raw_config self._blueprint_inputs = blueprint_inputs self._trace_config = trace_config - - @property - def name(self): - """Name of the automation.""" - return self._name - - @property - def unique_id(self): - """Return unique ID.""" - return self._id - - @property - def should_poll(self): - """No polling needed for automation entities.""" - return False + self._attr_unique_id = automation_id @property def extra_state_attributes(self): @@ -330,8 +317,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): } if self.action_script.supports_max: attrs[ATTR_MAX] = self.action_script.max_runs - if self._id is not None: - attrs[CONF_ID] = self._id + if self.unique_id is not None: + attrs[CONF_ID] = self.unique_id return attrs @property @@ -496,7 +483,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.async_set_context(trigger_context) event_data = { - ATTR_NAME: self._name, + ATTR_NAME: self.name, ATTR_ENTITY_ID: self.entity_id, } if "trigger" in variables and "description" in variables["trigger"]: @@ -580,7 +567,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Set up the triggers.""" def log_cb(level, msg, **kwargs): - self._logger.log(level, "%s %s", msg, self._name, **kwargs) + self._logger.log(level, "%s %s", msg, self.name, **kwargs) variables = None if self._trigger_variables: @@ -597,7 +584,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._trigger_config, self.async_trigger, DOMAIN, - self._name, + str(self.name), log_cb, home_assistant_start, variables, From 7aaa08f153d4814e99a9cda9fdc36f2758170d44 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Tue, 13 Jul 2021 18:37:09 +0200 Subject: [PATCH 261/818] Add binary_sensor support to Freedompro (#52717) * Update Freedompro * change _attr_unique_id with unique_id --- .../components/freedompro/__init__.py | 2 +- .../components/freedompro/binary_sensor.py | 86 +++++++++++++ tests/components/freedompro/const.py | 2 +- .../freedompro/test_binary_sensor.py | 114 ++++++++++++++++++ 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/freedompro/binary_sensor.py create mode 100644 tests/components/freedompro/test_binary_sensor.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index ddb6688a127..6b377f51560 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py new file mode 100644 index 00000000000..b629f4c0af4 --- /dev/null +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -0,0 +1,86 @@ +"""Support for Freedompro binary_sensor.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +DEVICE_CLASS_MAP = { + "smokeSensor": DEVICE_CLASS_SMOKE, + "occupancySensor": DEVICE_CLASS_OCCUPANCY, + "motionSensor": DEVICE_CLASS_MOTION, + "contactSensor": DEVICE_CLASS_OPENING, +} + +DEVICE_KEY_MAP = { + "smokeSensor": "smokeDetected", + "occupancySensor": "occupancyDetected", + "motionSensor": "motionDetected", + "contactSensor": "contactSensorState", +} + +SUPPORTED_SENSORS = {"smokeSensor", "occupancySensor", "motionSensor", "contactSensor"} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro binary_sensor.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] in SUPPORTED_SENSORS + ) + + +class Device(CoordinatorEntity, BinarySensorEntity): + """Representation of an Freedompro binary_sensor.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro binary_sensor.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": self._type, + "manufacturer": "Freedompro", + } + self._attr_device_class = DEVICE_CLASS_MAP[self._type] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + self._attr_is_on = state[DEVICE_KEY_MAP[self._type]] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py index 0db9d5884b1..2f59d190e2c 100644 --- a/tests/components/freedompro/const.py +++ b/tests/components/freedompro/const.py @@ -131,7 +131,7 @@ DEVICES_STATE = [ { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", "type": "contactSensor", - "state": {"contactSensorState": True}, + "state": {"contactSensorState": False}, "online": True, }, { diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py new file mode 100644 index 00000000000..785a6b03212 --- /dev/null +++ b/tests/components/freedompro/test_binary_sensor.py @@ -0,0 +1,114 @@ +"""Tests for the Freedompro binary sensor.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "binary_sensor.doorway_motion_sensor", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK", + "Doorway motion sensor", + "motionSensor", + ), + ( + "binary_sensor.contact_sensor_living_room", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", + "Contact sensor living room", + "contactSensor", + ), + ( + "binary_sensor.living_room_occupancy_sensor", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS", + "Living room occupancy sensor", + "occupancySensor", + ), + ( + "binary_sensor.smoke_sensor_kitchen", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY", + "Smoke sensor kitchen", + "smokeSensor", + ), + ], +) +async def test_binary_sensor_get_state( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test states of the binary_sensor.""" + init_integration + registry = er.async_get(hass) + registry_device = dr.async_get(hass) + + device = registry_device.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == name + assert device.model == model + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_OFF + + with patch( + "homeassistant.components.freedompro.get_states", + return_value=[], + ): + + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_OFF + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + if state_response["type"] == "smokeSensor": + state_response["state"]["smokeDetected"] = True + if state_response["type"] == "occupancySensor": + state_response["state"]["occupancyDetected"] = True + if state_response["type"] == "motionSensor": + state_response["state"]["motionDetected"] = True + if state_response["type"] == "contactSensor": + state_response["state"]["contactSensorState"] = True + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_ON From 987c7a289ab678a71f7c887383435217642832e1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 13 Jul 2021 13:31:17 -0400 Subject: [PATCH 262/818] Update ZHA to support zigpy 0.34.0 device initialization (#52610) * Handle `None` node descriptors * Skip loading uninitialized devices * Fix unit test incorrectly handling unset cluster `ep_attribute` * Revert filtering devices by status during startup --- homeassistant/components/zha/core/device.py | 39 ++++++++++++++------- tests/components/zha/common.py | 17 +++++---- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 0c572bfba8a..c6166419e39 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -183,11 +183,12 @@ class ZHADevice(LogMixin): return self._zigpy_device.model @property - def manufacturer_code(self): + def manufacturer_code(self) -> int | None: """Return the manufacturer code for the device.""" - if self._zigpy_device.node_desc.is_valid: - return self._zigpy_device.node_desc.manufacturer_code - return None + if self._zigpy_device.node_desc is None: + return None + + return self._zigpy_device.node_desc.manufacturer_code @property def nwk(self): @@ -210,17 +211,20 @@ class ZHADevice(LogMixin): return self._zigpy_device.last_seen @property - def is_mains_powered(self): + def is_mains_powered(self) -> bool | None: """Return true if device is mains powered.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_mains_powered @property - def device_type(self): + def device_type(self) -> str: """Return the logical device type for the device.""" - node_descriptor = self._zigpy_device.node_desc - return ( - node_descriptor.logical_type.name if node_descriptor.is_valid else UNKNOWN - ) + if self._zigpy_device.node_desc is None: + return UNKNOWN + + return self._zigpy_device.node_desc.logical_type.name @property def power_source(self): @@ -230,18 +234,27 @@ class ZHADevice(LogMixin): ) @property - def is_router(self): + def is_router(self) -> bool | None: """Return true if this is a routing capable device.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_router @property - def is_coordinator(self): + def is_coordinator(self) -> bool | None: """Return true if this device represents the coordinator.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_coordinator @property - def is_end_device(self): + def is_end_device(self) -> bool | None: """Return true if this device is an end device.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_end_device @property diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index eb65cc4fd2e..5180e9dbc07 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,8 +3,8 @@ import asyncio import time from unittest.mock import AsyncMock, Mock -from zigpy.device import Device as zigpy_dev -from zigpy.endpoint import Endpoint as zigpy_ep +import zigpy.device as zigpy_dev +import zigpy.endpoint as zigpy_ep import zigpy.profiles.zha import zigpy.types import zigpy.zcl @@ -27,7 +27,7 @@ class FakeEndpoint: self.out_clusters = {} self._cluster_attr = {} self.member_of = {} - self.status = 1 + self.status = zigpy_ep.Status.ZDO_INIT self.manufacturer = manufacturer self.model = model self.profile_id = zigpy.profiles.zha.PROFILE_ID @@ -57,7 +57,7 @@ class FakeEndpoint: @property def __class__(self): """Fake being Zigpy endpoint.""" - return zigpy_ep + return zigpy_ep.Endpoint @property def unique_id(self): @@ -65,8 +65,8 @@ class FakeEndpoint: return self.device.ieee, self.endpoint_id -FakeEndpoint.add_to_group = zigpy_ep.add_to_group -FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group +FakeEndpoint.add_to_group = zigpy_ep.Endpoint.add_to_group +FakeEndpoint.remove_from_group = zigpy_ep.Endpoint.remove_from_group def patch_cluster(cluster): @@ -125,12 +125,11 @@ class FakeDevice: self.lqi = 255 self.rssi = 8 self.last_seen = time.time() - self.status = 2 + self.status = zigpy_dev.Status.ENDPOINTS_INIT self.initializing = False self.skip_configuration = False self.manufacturer = manufacturer self.model = model - self.node_desc = zigpy.zdo.types.NodeDescriptor() self.remove_from_group = AsyncMock() if node_desc is None: node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" @@ -138,7 +137,7 @@ class FakeDevice: self.neighbors = [] -FakeDevice.add_to_group = zigpy_dev.add_to_group +FakeDevice.add_to_group = zigpy_dev.Device.add_to_group def get_zha_gateway(hass): From 3d419d1e8a01bc5b222800850b243e10542f0282 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 13 Jul 2021 13:32:55 -0400 Subject: [PATCH 263/818] Fix flume converagerc (#52975) --- .coveragerc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 685642612fc..bcfa7fc8e4f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -319,7 +319,8 @@ omit = homeassistant/components/flick_electric/const.py homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py - homeassistant/components/flume/* + homeassistant/components/flume/__init__.py + homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/sensor.py homeassistant/components/flux_led/light.py From 23b0633ae229344805c1f3830723269c73c8598d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Tue, 13 Jul 2021 19:33:53 +0200 Subject: [PATCH 264/818] Bump pysma to 0.6.4 (#52973) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index a48b9ba74ce..985a0506574 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.2"], + "requirements": ["pysma==0.6.4"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d2f1af82d2a..7b1af1d64fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1747,7 +1747,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.2 +pysma==0.6.4 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfa79b8eb08..2e6f03f3aa5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -989,7 +989,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.2 +pysma==0.6.4 # homeassistant.components.smappee pysmappee==0.2.25 From 777fec62a5d2499db49dd4fd012539673b578ce5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Jul 2021 19:35:55 +0200 Subject: [PATCH 265/818] Set device class for climacell temperature sensors (#52965) --- homeassistant/components/climacell/const.py | 4 ++++ homeassistant/components/climacell/sensor.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 057cef5e993..7f93b9a6507 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -26,6 +26,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_NAME, CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -34,6 +35,7 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + DEVICE_CLASS_TEMPERATURE, IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, @@ -161,6 +163,7 @@ CC_SENSOR_TYPES = [ val, TEMP_FAHRENHEIT, TEMP_CELSIUS ), ATTR_IS_METRIC_CHECK: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, }, { ATTR_FIELD: CC_ATTR_DEW_POINT, @@ -171,6 +174,7 @@ CC_SENSOR_TYPES = [ val, TEMP_FAHRENHEIT, TEMP_CELSIUS ), ATTR_IS_METRIC_CHECK: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, }, { ATTR_FIELD: CC_ATTR_PRESSURE_SURFACE_LEVEL, diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 2c620cc65a1..e5d9a2cdb75 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, ATTR_NAME, CONF_API_VERSION, CONF_NAME, @@ -73,6 +74,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): """Initialize ClimaCell Sensor Entity.""" super().__init__(config_entry, coordinator, api_version) self.sensor_type = sensor_type + self._attr_device_class = self.sensor_type.get(ATTR_DEVICE_CLASS) @property def entity_registry_enabled_default(self) -> bool: From 026ca4e4e47bb169aee2dd612a03bba4f7d28512 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jul 2021 13:56:41 -0400 Subject: [PATCH 266/818] Additional fixes for siren platform (#52971) --- homeassistant/components/siren/__init__.py | 7 +++---- tests/components/siren/test_init.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index e5e03088a49..68770daf835 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -63,11 +63,10 @@ def process_turn_on_params( if not supported_features & SUPPORT_TONES: params.pop(ATTR_TONE, None) - elif ATTR_TONE in params and ( - not siren.available_tones - or (tone := params[ATTR_TONE]) not in siren.available_tones + elif (tone := params.get(ATTR_TONE)) is not None and ( + not siren.available_tones or tone not in siren.available_tones ): - raise ValueError(f"Tone {tone} is not a valid tone for this device") + raise ValueError(f"Invalid tone received for entity {siren.entity_id}: {tone}") if not supported_features & SUPPORT_DURATION: params.pop(ATTR_DURATION, None) diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index e3b8bded6c8..6f90b8a9fcd 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -9,10 +9,9 @@ class MockSirenEntity(SirenEntity): _attr_is_on = True - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return 0 + def __init__(self, supported_features: int = 0) -> None: + """Initialize mock siren entity.""" + self._attr_supported_features = supported_features async def test_sync_turn_on(hass): From 2b65501ca7d9a004fa41307c3a618b98ac02eb38 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 13 Jul 2021 14:06:41 -0400 Subject: [PATCH 267/818] Use entity class attributes for aquostv (#52670) * Use entity class attributes for aquostv * fix * fix * Tweak * tweak --- .../components/aquostv/media_player.py | 68 ++++--------------- 1 file changed, 15 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 35c7e2ae646..50dd70bddcc 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -124,33 +124,30 @@ def _retry(func): class SharpAquosTVDevice(MediaPlayerEntity): """Representation of a Aquos TV.""" + _attr_source_list = list(SOURCES.values()) + _attr_supported_features = SUPPORT_SHARPTV + def __init__(self, name, remote, power_on_enabled=False): """Initialize the aquos device.""" - self._supported_features = SUPPORT_SHARPTV self._power_on_enabled = power_on_enabled - if self._power_on_enabled: - self._supported_features |= SUPPORT_TURN_ON + if power_on_enabled: + self._attr_supported_features |= SUPPORT_TURN_ON # Save a reference to the imported class - self._name = name + self._attr_name = name # Assume that the TV is not muted - self._muted = False - self._state = None self._remote = remote - self._volume = 0 - self._source = None - self._source_list = list(SOURCES.values()) def set_state(self, state): """Set TV state.""" - self._state = state + self._attr_state = state @_retry def update(self): """Retrieve the latest data.""" if self._remote.power() == 1: - self._state = STATE_ON + self._attr_state = STATE_ON else: - self._state = STATE_OFF + self._attr_state = STATE_OFF # Set TV to be able to remotely power on if self._power_on_enabled: self._remote.power_on_command_settings(2) @@ -158,48 +155,13 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._remote.power_on_command_settings(0) # Get mute state if self._remote.mute() == 2: - self._muted = False + self._attr_is_volume_muted = False else: - self._muted = True + self._attr_is_volume_muted = True # Get source - self._source = SOURCES.get(self._remote.input()) + self._attr_source = SOURCES.get(self._remote.input()) # Get volume - self._volume = self._remote.volume() / 60 - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def source(self): - """Return the current source.""" - return self._source - - @property - def source_list(self): - """Return the source list.""" - return self._source_list - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return self._supported_features + self._attr_volume_level = self._remote.volume() / 60 @_retry def turn_off(self): @@ -209,12 +171,12 @@ class SharpAquosTVDevice(MediaPlayerEntity): @_retry def volume_up(self): """Volume up the media player.""" - self._remote.volume(int(self._volume * 60) + 2) + self._remote.volume(int(self.volume_level * 60) + 2) @_retry def volume_down(self): """Volume down media player.""" - self._remote.volume(int(self._volume * 60) - 2) + self._remote.volume(int(self.volume_level * 60) - 2) @_retry def set_volume_level(self, volume): From 7f0eff8230d610ba87015e210fe2547afdf151f8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 13 Jul 2021 14:08:22 -0400 Subject: [PATCH 268/818] Use entity class attributes for Blackbird (#52889) * Use entity class attributes for blackbird * rework * undo media_title --- .../components/blackbird/media_player.py | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index 9ae696a5276..5407580612e 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -131,6 +131,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlackbirdZone(MediaPlayerEntity): """Representation of a Blackbird matrix zone.""" + _attr_supported_features = SUPPORT_BLACKBIRD + def __init__(self, blackbird, sources, zone_id, zone_name): """Initialize new zone.""" self._blackbird = blackbird @@ -139,55 +141,28 @@ class BlackbirdZone(MediaPlayerEntity): # dict source name -> source_id self._source_name_id = {v: k for k, v in sources.items()} # ordered list of all source names - self._source_names = sorted( + self._attr_source_list = sorted( self._source_name_id.keys(), key=lambda v: self._source_name_id[v] ) self._zone_id = zone_id - self._name = zone_name - self._state = None - self._source = None + self._attr_name = zone_name def update(self): """Retrieve latest state.""" state = self._blackbird.zone_status(self._zone_id) if not state: return - self._state = STATE_ON if state.power else STATE_OFF + self._attr_state = STATE_ON if state.power else STATE_OFF idx = state.av if idx in self._source_id_name: - self._source = self._source_id_name[idx] + self._attr_source = self._source_id_name[idx] else: - self._source = None - - @property - def name(self): - """Return the name of the zone.""" - return self._name - - @property - def state(self): - """Return the state of the zone.""" - return self._state - - @property - def supported_features(self): - """Return flag of media commands that are supported.""" - return SUPPORT_BLACKBIRD + self._attr_source = None @property def media_title(self): """Return the current source as media title.""" - return self._source - - @property - def source(self): - """Return the current input source of the device.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self._source_names + return self.source def set_all_zones(self, source): """Set all zones to one source.""" From 794571efdddb415c465ee7c92ddb5017f61119ef Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jul 2021 14:13:56 -0400 Subject: [PATCH 269/818] Add missing device classes for climacell sensors (#52979) --- homeassistant/components/climacell/const.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 7f93b9a6507..f5d2ac02696 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -35,6 +35,8 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + DEVICE_CLASS_CO, + DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, @@ -185,6 +187,7 @@ CC_SENSOR_TYPES = [ val, PRESSURE_INHG, PRESSURE_HPA ), ATTR_IS_METRIC_CHECK: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, }, { ATTR_FIELD: CC_ATTR_SOLAR_GHI, @@ -265,6 +268,7 @@ CC_SENSOR_TYPES = [ ATTR_FIELD: CC_ATTR_CARBON_MONOXIDE, ATTR_NAME: "Carbon Monoxide", CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, }, { ATTR_FIELD: CC_ATTR_SULFUR_DIOXIDE, @@ -404,6 +408,7 @@ CC_V3_SENSOR_TYPES = [ ATTR_FIELD: CC_V3_ATTR_CARBON_MONOXIDE, ATTR_NAME: "Carbon Monoxide", CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, }, { ATTR_FIELD: CC_V3_ATTR_SULFUR_DIOXIDE, From ff56ede9607f6bfd395f69c2ded0f88c48ffbea9 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 13 Jul 2021 20:21:50 +0200 Subject: [PATCH 270/818] Bump python-fireservicerota to 0.0.43 (#52966) --- homeassistant/components/fireservicerota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json index f35be9e839f..1eea9fbfbf1 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -3,7 +3,7 @@ "name": "FireServiceRota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fireservicerota", - "requirements": ["pyfireservicerota==0.0.42"], + "requirements": ["pyfireservicerota==0.0.43"], "codeowners": ["@cyberjunky"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7b1af1d64fe..8645253f692 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1421,7 +1421,7 @@ pyezviz==0.1.8.9 pyfido==2.1.1 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.42 +pyfireservicerota==0.0.43 # homeassistant.components.flexit pyflexit==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e6f03f3aa5..7949d645156 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -789,7 +789,7 @@ pyezviz==0.1.8.9 pyfido==2.1.1 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.42 +pyfireservicerota==0.0.43 # homeassistant.components.flume pyflume==0.5.5 From f39f087b10f0e43a2b86f985b861f74733fa85cf Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 13 Jul 2021 13:22:31 -0500 Subject: [PATCH 271/818] More graceful exception handling in Plex library sensors (#52969) --- homeassistant/components/plex/sensor.py | 8 +++++++ tests/components/plex/test_sensor.py | 31 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 2bfd0d0d926..8ca72e8fb83 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -2,6 +2,7 @@ import logging from plexapi.exceptions import NotFound +import requests.exceptions from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.debounce import Debouncer @@ -148,6 +149,13 @@ class PlexLibrarySectionSensor(SensorEntity): self._attr_available = True except NotFound: self._attr_available = False + except requests.exceptions.RequestException as err: + _LOGGER.error( + "Could not update library sensor for '%s': %s", + self.library_section.title, + err, + ) + self._attr_available = False self.async_write_ha_state() def _update_state_and_attrs(self): diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index 5fa50892f32..39a2901e72d 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -1,6 +1,8 @@ """Tests for Plex sensors.""" from datetime import timedelta +import requests.exceptions + from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er @@ -15,6 +17,7 @@ LIBRARY_UPDATE_PAYLOAD = {"StatusNotification": [{"title": "Library scan complet async def test_library_sensor_values( hass, + caplog, setup_plex_server, mock_websocket, requests_mock, @@ -63,6 +66,34 @@ async def test_library_sensor_values( assert library_tv_sensor.attributes["seasons"] == 1 assert library_tv_sensor.attributes["shows"] == 1 + # Handle `requests` exception + requests_mock.get( + "/library/sections/2/all?includeCollections=0&type=2", + exc=requests.exceptions.ReadTimeout, + ) + trigger_plex_update( + mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD + ) + await hass.async_block_till_done() + + library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") + assert library_tv_sensor.state == STATE_UNAVAILABLE + + assert "Could not update library sensor" in caplog.text + + # Ensure sensor updates properly when it recovers + requests_mock.get( + "/library/sections/2/all?includeCollections=0&type=2", + text=library_tvshows_size, + ) + trigger_plex_update( + mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD + ) + await hass.async_block_till_done() + + library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") + assert library_tv_sensor.state == "10" + # Handle library deletion requests_mock.get( "/library/sections/2/all?includeCollections=0&type=2", status_code=404 From 960684346f7d46c6612a1feedd32496b425796d8 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 13 Jul 2021 14:27:04 -0400 Subject: [PATCH 272/818] Fix issue connecting to Insteon Hub v2 (#52970) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 353cd55c747..4643a8c662a 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.11" + "pyinsteon==1.0.12" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index 8645253f692..e7724fd1afb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1488,7 +1488,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.11 +pyinsteon==1.0.12 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7949d645156..9f4e2ab5855 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -835,7 +835,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.11 +pyinsteon==1.0.12 # homeassistant.components.ipma pyipma==2.0.5 From 19d3aa71ad7649a4a7f2e47b891d89a031db9f7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Jul 2021 21:21:45 +0200 Subject: [PATCH 273/818] Enable basic type checking for recorder (#52440) * Enable basic type checking for recorder * Tweak --- homeassistant/components/recorder/__init__.py | 8 +- homeassistant/components/recorder/models.py | 35 +++++++- .../components/recorder/statistics.py | 88 ++++++++++++++----- homeassistant/components/recorder/util.py | 10 ++- homeassistant/components/sensor/recorder.py | 2 +- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - 7 files changed, 108 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index c16d7a2d198..e9c12e5f88a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -324,7 +324,7 @@ class PerodicCleanupTask: class StatisticsTask(NamedTuple): """An object to insert into the recorder queue to run a statistics task.""" - start: datetime.datetime + start: datetime class WaitTask: @@ -358,7 +358,7 @@ class Recorder(threading.Thread): self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.async_db_ready = asyncio.Future() + self.async_db_ready: asyncio.Future = asyncio.Future() self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Any = None @@ -370,8 +370,8 @@ class Recorder(threading.Thread): self._timechanges_seen = 0 self._commits_without_expire = 0 self._keepalive_count = 0 - self._old_states = {} - self._pending_expunge = [] + self._old_states: dict[str, States] = {} + self._pending_expunge: list[States] = [] self.event_session = None self.get_session = None self._completed_first_database_setup = None diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index c77d824c64f..4fd51886246 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,10 @@ """Models for SQLAlchemy.""" +from __future__ import annotations + +from datetime import datetime import json import logging +from typing import TypedDict from sqlalchemy import ( Boolean, @@ -206,6 +210,17 @@ class States(Base): # type: ignore return None +class StatisticData(TypedDict, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + class Statistics(Base): # type: ignore """Statistics.""" @@ -230,7 +245,7 @@ class Statistics(Base): # type: ignore sum = Column(Float()) @staticmethod - def from_stats(metadata_id, start, stats): + def from_stats(metadata_id: str, start: datetime, stats: StatisticData): """Create object from a statistics.""" return Statistics( metadata_id=metadata_id, @@ -239,6 +254,14 @@ class Statistics(Base): # type: ignore ) +class StatisticMetaData(TypedDict, total=False): + """Statistic meta data class.""" + + unit_of_measurement: str | None + has_mean: bool + has_sum: bool + + class StatisticsMeta(Base): # type: ignore """Statistics meta data.""" @@ -251,7 +274,13 @@ class StatisticsMeta(Base): # type: ignore has_sum = Column(Boolean) @staticmethod - def from_meta(source, statistic_id, unit_of_measurement, has_mean, has_sum): + def from_meta( + source: str, + statistic_id: str, + unit_of_measurement: str | None, + has_mean: bool, + has_sum: bool, + ) -> StatisticsMeta: """Create object from meta data.""" return StatisticsMeta( source=source, @@ -340,7 +369,7 @@ def process_timestamp(ts): return dt_util.as_utc(ts) -def process_timestamp_to_utc_isoformat(ts): +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: """Process a timestamp into UTC isotime.""" if ts is None: return None diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4ddf380ee3a..2583b2f53b6 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -5,18 +5,26 @@ from collections import defaultdict from datetime import datetime, timedelta from itertools import groupby import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable from sqlalchemy import bindparam from sqlalchemy.ext import baked +from sqlalchemy.orm.scoping import scoped_session from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util +from homeassistant.util.unit_system import UnitSystem from .const import DOMAIN -from .models import Statistics, StatisticsMeta, process_timestamp_to_utc_isoformat +from .models import ( + StatisticMetaData, + Statistics, + StatisticsMeta, + process_timestamp_to_utc_isoformat, +) from .util import execute, retryable_database_job, session_scope if TYPE_CHECKING: @@ -60,20 +68,22 @@ UNIT_CONVERSIONS = { _LOGGER = logging.getLogger(__name__) -def async_setup(hass): +def async_setup(hass: HomeAssistant) -> None: """Set up the history hooks.""" hass.data[STATISTICS_BAKERY] = baked.bakery() hass.data[STATISTICS_META_BAKERY] = baked.bakery() -def get_start_time() -> datetime.datetime: +def get_start_time() -> datetime: """Return start time.""" last_hour = dt_util.utcnow() - timedelta(hours=1) start = last_hour.replace(minute=0, second=0, microsecond=0) return start -def _get_metadata_ids(hass, session, statistic_ids): +def _get_metadata_ids( + hass: HomeAssistant, session: scoped_session, statistic_ids: list[str] +) -> list[str]: """Resolve metadata_id for a list of statistic_ids.""" baked_query = hass.data[STATISTICS_META_BAKERY]( lambda session: session.query(*QUERY_STATISTIC_META) @@ -83,10 +93,15 @@ def _get_metadata_ids(hass, session, statistic_ids): ) result = execute(baked_query(session).params(statistic_ids=statistic_ids)) - return [id for id, _, _ in result] + return [id for id, _, _ in result] if result else [] -def _get_or_add_metadata_id(hass, session, statistic_id, metadata): +def _get_or_add_metadata_id( + hass: HomeAssistant, + session: scoped_session, + statistic_id: str, + metadata: StatisticMetaData, +) -> str: """Get metadata_id for a statistic_id, add if it doesn't exist.""" metadata_id = _get_metadata_ids(hass, session, [statistic_id]) if not metadata_id: @@ -101,7 +116,7 @@ def _get_or_add_metadata_id(hass, session, statistic_id, metadata): @retryable_database_job("statistics") -def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: +def compile_statistics(instance: Recorder, start: datetime) -> bool: """Compile statistics.""" start = dt_util.as_utc(start) end = start + timedelta(hours=1) @@ -126,10 +141,15 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: return True -def _get_metadata(hass, session, statistic_ids, statistic_type): +def _get_metadata( + hass: HomeAssistant, + session: scoped_session, + statistic_ids: list[str] | None, + statistic_type: str | None, +) -> dict[str, dict[str, str]]: """Fetch meta data.""" - def _meta(metas, wanted_metadata_id): + def _meta(metas: list, wanted_metadata_id: str) -> dict[str, str] | None: meta = None for metadata_id, statistic_id, unit in metas: if metadata_id == wanted_metadata_id: @@ -150,12 +170,19 @@ def _get_metadata(hass, session, statistic_ids, statistic_type): elif statistic_type is not None: return {} result = execute(baked_query(session).params(statistic_ids=statistic_ids)) + if not result: + return {} metadata_ids = [metadata[0] for metadata in result] - return {id: _meta(result, id) for id in metadata_ids} + metadata = {} + for _id in metadata_ids: + meta = _meta(result, _id) + if meta: + metadata[_id] = meta + return metadata -def _configured_unit(unit: str, units) -> str: +def _configured_unit(unit: str, units: UnitSystem) -> str: """Return the pressure and temperature units configured by the user.""" if unit == PRESSURE_PA: return units.pressure_unit @@ -164,7 +191,9 @@ def _configured_unit(unit: str, units) -> str: return unit -def list_statistic_ids(hass, statistic_type=None): +def list_statistic_ids( + hass: HomeAssistant, statistic_type: str | None = None +) -> list[dict[str, str] | None]: """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: @@ -177,7 +206,12 @@ def list_statistic_ids(hass, statistic_type=None): return list(metadata.values()) -def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): +def statistics_during_period( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + statistic_ids: list[str] | None = None, +) -> dict[str, list[dict[str, str]]]: """Return states changes during UTC period start_time - end_time.""" metadata = None with session_scope(hass=hass) as session: @@ -208,10 +242,14 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None start_time=start_time, end_time=end_time, metadata_ids=metadata_ids ) ) + if not stats: + return {} return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata) -def get_last_statistics(hass, number_of_stats, statistic_id): +def get_last_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str +) -> dict[str, list[dict]]: """Return the last number_of_stats statistics for a statistic_id.""" statistic_ids = [statistic_id] with session_scope(hass=hass) as session: @@ -237,18 +275,20 @@ def get_last_statistics(hass, number_of_stats, statistic_id): number_of_stats=number_of_stats, metadata_id=metadata_id ) ) + if not stats: + return {} return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata) def _sorted_statistics_to_dict( - hass, - stats, - statistic_ids, - metadata, -): + hass: HomeAssistant, + stats: list, + statistic_ids: list[str] | None, + metadata: dict[str, dict[str, str]], +) -> dict[str, list[dict]]: """Convert SQL results into JSON friendly data structure.""" - result = defaultdict(list) + result: dict = defaultdict(list) units = hass.config.units # Set all statistic IDs to empty lists in result set to maintain the order @@ -260,10 +300,12 @@ def _sorted_statistics_to_dict( _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat # Append all statistic entries, and do unit conversion - for meta_id, group in groupby(stats, lambda state: state.metadata_id): + for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] - convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) + convert: Callable[[Any, Any], float | None] = UNIT_CONVERSIONS.get( + unit, lambda x, units: x # type: ignore + ) ent_results = result[meta_id] ent_results.extend( { diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 80c90ccaa20..225eee6867f 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -8,7 +8,7 @@ import functools import logging import os import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session @@ -91,7 +91,7 @@ def commit(session, work): return False -def execute(qry, to_native=False, validate_entity_ids=True): +def execute(qry, to_native=False, validate_entity_ids=True) -> list | None: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. @@ -135,6 +135,8 @@ def execute(qry, to_native=False, validate_entity_ids=True): raise time.sleep(QUERY_RETRY_WAIT) + return None + def validate_or_move_away_sqlite_database(dburl: str) -> bool: """Ensure that the database is valid or move it away.""" @@ -288,13 +290,13 @@ def end_incomplete_runs(session, start_time): session.add(run) -def retryable_database_job(description: str): +def retryable_database_job(description: str) -> Callable: """Try to execute a database job. The job should return True if it finished, and False if it needs to be rescheduled. """ - def decorator(job: callable): + def decorator(job: Callable) -> Callable: @functools.wraps(job) def wrapper(instance: Recorder, *args, **kwargs): try: diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index bbd49814076..485674ec728 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -244,7 +244,7 @@ def compile_statistics( last_reset = old_last_reset = None new_state = old_state = None _sum = 0 - last_stats = statistics.get_last_statistics(hass, 1, entity_id) # type: ignore + last_stats = statistics.get_last_statistics(hass, 1, entity_id) if entity_id in last_stats: # We have compiled history for this sensor before, use that as a starting point last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] diff --git a/mypy.ini b/mypy.ini index 11330acad6f..03fc0781027 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1470,9 +1470,6 @@ ignore_errors = true [mypy-homeassistant.components.recollect_waste.*] ignore_errors = true -[mypy-homeassistant.components.recorder.*] -ignore_errors = true - [mypy-homeassistant.components.reddit.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 447bbab51f6..d28f56bdea4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -152,7 +152,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.rachio.*", "homeassistant.components.rainmachine.*", "homeassistant.components.recollect_waste.*", - "homeassistant.components.recorder.*", "homeassistant.components.reddit.*", "homeassistant.components.ring.*", "homeassistant.components.rpi_power.*", From 12ac66645900b3227af29b62f7f35ea722932589 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 13 Jul 2021 21:45:42 +0200 Subject: [PATCH 274/818] only allow one active call in each platform. (#52823) --- .coveragerc | 3 +++ homeassistant/components/modbus/base_platform.py | 6 ++++++ homeassistant/components/modbus/binary_sensor.py | 6 ++++++ homeassistant/components/modbus/climate.py | 7 ++++++- homeassistant/components/modbus/cover.py | 5 +++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index bcfa7fc8e4f..6f6adee207a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -634,6 +634,9 @@ omit = homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* + homeassistant/components/modbus/base_platform.py + homeassistant/components/modbus/binary_sensor.py + homeassistant/components/modbus/cover.py homeassistant/components/modbus/climate.py homeassistant/components/modbus/modbus.py homeassistant/components/modbus/validators.py diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 9c21ba3970c..c382923e15d 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -56,6 +56,7 @@ class BasePlatform(Entity): self._value = None self._available = True self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) + self._call_active = False @abstractmethod async def async_update(self, now=None): @@ -174,9 +175,14 @@ class BaseSwitch(BasePlatform, RestoreEntity): self.async_write_ha_state() return + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True result = await self._hub.async_pymodbus_call( self._slave, self._verify_address, 1, self._verify_type ) + self._call_active = False if result is None: self._available = False self.async_write_ha_state() diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index bc586e2f24d..0188210be8a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -54,9 +54,15 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): async def async_update(self, now=None): """Update the state of the sensor.""" + + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True result = await self._hub.async_pymodbus_call( self._slave, self._address, 1, self._input_type ) + self._call_active = False if result is None: self._available = False self.async_write_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 54894a4227c..dbec27c3af6 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -194,13 +194,18 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): """Update Target & Current Temperature.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval + + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True self._target_temperature = await self._async_read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) self._current_temperature = await self._async_read_register( self._input_type, self._address ) - + self._call_active = False self.async_write_ha_state() async def _async_read_register(self, register_type, register) -> float | None: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 88c8fd77ae8..bd150434dc1 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -149,9 +149,14 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): """Update the state of the cover.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True result = await self._hub.async_pymodbus_call( self._slave, self._address, 1, self._input_type ) + self._call_active = False if result is None: self._available = False self.async_write_ha_state() From d76607e945f73be0c4742e48bb24d5c5f4d34d1f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 13 Jul 2021 15:56:34 -0400 Subject: [PATCH 275/818] Use entity class attributes for august (#52744) --- .../components/august/binary_sensor.py | 74 ++++--------------- homeassistant/components/august/camera.py | 12 +-- homeassistant/components/august/entity.py | 28 +++---- homeassistant/components/august/lock.py | 53 ++++--------- homeassistant/components/august/sensor.py | 62 +++------------- 5 files changed, 52 insertions(+), 177 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index e72d4b186a5..97faf444f3b 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -126,34 +126,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): """Representation of an August Door binary sensor.""" + _attr_device_class = DEVICE_CLASS_DOOR + def __init__(self, data, sensor_type, device): """Initialize the sensor.""" super().__init__(data, device) self._data = data self._sensor_type = sensor_type self._device = device + self._attr_name = f"{device.device_name} Open" + self._attr_unique_id = f"{self._device_id}_open" self._update_from_data() - @property - def available(self): - """Return the availability of this sensor.""" - return self._detail.bridge_is_online - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._detail.door_state == LockDoorStatus.OPEN - - @property - def device_class(self): - """Return the class of this device.""" - return DEVICE_CLASS_DOOR - - @property - def name(self): - """Return the name of the binary sensor.""" - return f"{self._device.device_name} Open" - @callback def _update_from_data(self): """Get the latest state of the sensor and update activity.""" @@ -173,11 +157,8 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): if bridge_activity is not None: update_lock_detail_from_activity(self._detail, bridge_activity) - - @property - def unique_id(self) -> str: - """Get the unique of the door open binary sensor.""" - return f"{self._device_id}_open" + self._attr_available = self._detail.bridge_is_online + self._attr_is_on = self._detail.door_state == LockDoorStatus.OPEN class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): @@ -189,36 +170,18 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self._check_for_off_update_listener = None self._data = data self._sensor_type = sensor_type - self._device = device - self._state = None - self._available = False + self._attr_device_class = self._sensor_config[SENSOR_DEVICE_CLASS] + self._attr_name = f"{device.device_name} {self._sensor_config[SENSOR_NAME]}" + self._attr_unique_id = ( + f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}" + ) self._update_from_data() - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - @property def _sensor_config(self): """Return the config for the sensor.""" return SENSOR_TYPES_DOORBELL[self._sensor_type] - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._sensor_config[SENSOR_DEVICE_CLASS] - - @property - def name(self): - """Return the name of the binary sensor.""" - return f"{self._device.device_name} {self._sensor_config[SENSOR_NAME]}" - @property def _state_provider(self): """Return the state provider for the binary sensor.""" @@ -233,19 +196,19 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): def _update_from_data(self): """Get the latest state of the sensor.""" self._cancel_any_pending_updates() - self._state = self._state_provider(self._data, self._detail) + self._attr_is_on = self._state_provider(self._data, self._detail) if self._is_time_based: - self._available = _retrieve_online_state(self._data, self._detail) + self._attr_available = _retrieve_online_state(self._data, self._detail) self._schedule_update_to_recheck_turn_off_sensor() else: - self._available = True + self._attr_available = True def _schedule_update_to_recheck_turn_off_sensor(self): """Schedule an update to recheck the sensor to see if it is ready to turn off.""" # If the sensor is already off there is nothing to do - if not self._state: + if not self.is_on: return # self.hass is only available after setup is completed @@ -258,7 +221,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Timer callback for sensor update.""" self._check_for_off_update_listener = None self._update_from_data() - if not self._state: + if not self.is_on: self.async_write_ha_state() self._check_for_off_update_listener = async_call_later( @@ -277,8 +240,3 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" self._schedule_update_to_recheck_turn_off_sensor() await super().async_added_to_hass() - - @property - def unique_id(self) -> str: - """Get the unique id of the doorbell sensor.""" - return f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}" diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index daaa7624aa3..6bb47a06eee 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -35,11 +35,8 @@ class AugustCamera(AugustEntityMixin, Camera): self._session = session self._image_url = None self._image_content = None - - @property - def name(self): - """Return the name of this device.""" - return f"{self._device.device_name} Camera" + self._attr_name = f"{device.device_name} Camera" + self._attr_unique_id = f"{self._device_id:s}_camera" @property def is_recording(self): @@ -81,8 +78,3 @@ class AugustCamera(AugustEntityMixin, Camera): self._session, timeout=self._timeout ) return self._image_content - - @property - def unique_id(self) -> str: - """Get the unique id of the camera.""" - return f"{self._device_id:s}_camera" diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index b2a93948449..af8c858a1d4 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -11,16 +11,21 @@ DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] class AugustEntityMixin(Entity): """Base implementation for August device.""" + _attr_should_poll = False + def __init__(self, data, device): """Initialize an August device.""" super().__init__() self._data = data self._device = device - - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False + self._attr_device_info = { + "identifiers": {(DOMAIN, self._device_id)}, + "name": device.device_name, + "manufacturer": MANUFACTURER, + "sw_version": self._detail.firmware_version, + "model": self._detail.model, + "suggested_area": _remove_device_types(device.device_name, DEVICE_TYPES), + } @property def _device_id(self): @@ -30,19 +35,6 @@ class AugustEntityMixin(Entity): def _detail(self): return self._data.get_device_detail(self._device.device_id) - @property - def device_info(self): - """Return the device_info of the device.""" - name = self._device.device_name - return { - "identifiers": {(DOMAIN, self._device_id)}, - "name": name, - "manufacturer": MANUFACTURER, - "sw_version": self._detail.firmware_version, - "model": self._detail.model, - "suggested_area": _remove_device_types(name, DEVICE_TYPES), - } - @callback def _update_from_data_and_write_state(self): self._update_from_data() diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 6e4ee7e6f5c..e74eded3557 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -31,8 +31,8 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): self._data = data self._device = device self._lock_status = None - self._changed_by = None - self._available = False + self._attr_name = device.device_name + self._attr_unique_id = f"{self._device_id:s}_lock" self._update_from_data() async def async_lock(self, **kwargs): @@ -56,7 +56,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): self._data.async_signal_device_id_update(self._device_id) def _update_lock_status_from_detail(self): - self._available = self._detail.bridge_is_online + self._attr_available = self._detail.bridge_is_online if self._lock_status != self._detail.lock_status: self._lock_status = self._detail.lock_status @@ -72,7 +72,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): ) if lock_activity is not None: - self._changed_by = lock_activity.operated_by + self._attr_changed_by = lock_activity.operated_by update_lock_detail_from_activity(self._detail, lock_activity) # If the source is pubnub the lock must be online since its a live update if lock_activity.source == SOURCE_PUBNUB: @@ -86,38 +86,18 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): update_lock_detail_from_activity(self._detail, bridge_activity) self._update_lock_status_from_detail() - - @property - def name(self): - """Return the name of this device.""" - return self._device.device_name - - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def is_locked(self): - """Return true if device is on.""" if self._lock_status is None or self._lock_status is LockStatus.UNKNOWN: - return None - return self._lock_status is LockStatus.LOCKED - - @property - def changed_by(self): - """Last change triggered by.""" - return self._changed_by - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attributes = {ATTR_BATTERY_LEVEL: self._detail.battery_level} + self._attr_is_locked = None + else: + self._attr_is_locked = self._lock_status is LockStatus.LOCKED + self._attr_extra_state_attributes = { + ATTR_BATTERY_LEVEL: self._detail.battery_level + } if self._detail.keypad is not None: - attributes["keypad_battery_level"] = self._detail.keypad.battery_level - - return attributes + self._attr_extra_state_attributes[ + "keypad_battery_level" + ] = self._detail.keypad.battery_level async def async_added_to_hass(self): """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" @@ -128,9 +108,4 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): return if ATTR_CHANGED_BY in last_state.attributes: - self._changed_by = last_state.attributes[ATTR_CHANGED_BY] - - @property - def unique_id(self) -> str: - """Get the unique id of the lock.""" - return f"{self._device_id:s}_lock" + self._attr_changed_by = last_state.attributes[ATTR_CHANGED_BY] diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 1d973a83fc3..a174964f349 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -125,25 +125,13 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): super().__init__(data, device) self._data = data self._device = device - self._state = None self._operated_remote = None self._operated_keypad = None self._operated_autorelock = None self._operated_time = None - self._available = False self._entity_picture = None self._update_from_data() - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - @property def name(self): """Return the name of the sensor.""" @@ -156,9 +144,9 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._device_id, {ActivityType.LOCK_OPERATION} ) - self._available = True + self._attr_available = True if lock_activity is not None: - self._state = lock_activity.operated_by + self._attr_state = lock_activity.operated_by self._operated_remote = lock_activity.operated_remote self._operated_keypad = lock_activity.operated_keypad self._operated_autorelock = lock_activity.operated_autorelock @@ -195,7 +183,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): if not last_state or last_state.state == STATE_UNAVAILABLE: return - self._state = last_state.state + self._attr_state = last_state.state if ATTR_ENTITY_PICTURE in last_state.attributes: self._entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] if ATTR_OPERATION_REMOTE in last_state.attributes: @@ -219,54 +207,24 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): class AugustBatterySensor(AugustEntityMixin, SensorEntity): """Representation of an August sensor.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, data, sensor_type, device, old_device): """Initialize the sensor.""" super().__init__(data, device) - self._data = data self._sensor_type = sensor_type - self._device = device self._old_device = old_device - self._state = None - self._available = False + self._attr_name = f"{device.device_name} Battery" + self._attr_unique_id = f"{self._device_id}_{sensor_type}" self._update_from_data() - @property - def available(self): - """Return the availability of this sensor.""" - return self._available - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return PERCENTAGE - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_BATTERY - - @property - def name(self): - """Return the name of the sensor.""" - device_name = self._device.device_name - return f"{device_name} Battery" - @callback def _update_from_data(self): """Get the latest state of the sensor.""" state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"] - self._state = state_provider(self._detail) - self._available = self._state is not None - - @property - def unique_id(self) -> str: - """Get the unique id of the device sensor.""" - return f"{self._device_id}_{self._sensor_type}" + self._attr_state = state_provider(self._detail) + self._attr_available = self._attr_state is not None @property def old_unique_id(self) -> str: From 9cd6a9626e1f9bce0779d08ad267f00cd6feb007 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Wed, 14 Jul 2021 00:10:23 +0200 Subject: [PATCH 276/818] Update pyrainbird to 0.4.3 (#52990) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 120e38e8058..d7d3c064ad7 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -2,7 +2,7 @@ "domain": "rainbird", "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==0.4.2"], + "requirements": ["pyrainbird==0.4.3"], "codeowners": ["@konikvranik"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index e7724fd1afb..db305130f24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1694,7 +1694,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.4.2 +pyrainbird==0.4.3 # homeassistant.components.recswitch pyrecswitch==1.0.2 From 7b7062dded1e78db81c7e974330f67a9d176c6e9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 14 Jul 2021 00:10:16 +0000 Subject: [PATCH 277/818] [ci skip] Translation update --- homeassistant/components/huawei_lte/translations/ca.json | 5 +++-- homeassistant/components/huawei_lte/translations/et.json | 5 +++-- homeassistant/components/huawei_lte/translations/pl.json | 5 +++-- homeassistant/components/samsungtv/translations/cs.json | 9 +++++++-- homeassistant/components/sonos/translations/ca.json | 1 + homeassistant/components/sonos/translations/et.json | 1 + homeassistant/components/sonos/translations/nl.json | 1 + homeassistant/components/sonos/translations/pl.json | 1 + homeassistant/components/sonos/translations/ru.json | 1 + homeassistant/components/sonos/translations/zh-Hant.json | 1 + homeassistant/components/syncthru/translations/cs.json | 2 +- homeassistant/components/zwave_js/translations/ca.json | 7 +++++++ homeassistant/components/zwave_js/translations/et.json | 7 +++++++ homeassistant/components/zwave_js/translations/pl.json | 7 +++++++ 14 files changed, 44 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 92966ca7eeb..0347398ce8b 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Nom d'usuari" }, - "description": "Introdueix les dades d'acc\u00e9s del dispositiu. El nom d'usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", + "description": "Introdueix les dades d'acc\u00e9s del dispositiu.", "title": "Configuraci\u00f3 de Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Nom del servei de notificacions (reinici necessari si canvia)", "recipient": "Destinataris de notificacions SMS", "track_new_devices": "Segueix dispositius nous", - "track_wired_clients": "Segueix els clients connectats a la xarxa per cable" + "track_wired_clients": "Segueix els clients connectats a la xarxa per cable", + "unauthenticated_mode": "Mode no autenticat (canviar requereix tornar a carregar)" } } } diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 8cd61842767..955d5cc2c5a 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Kasutajanimi" }, - "description": "Sisesta seadmele juurdep\u00e4\u00e4su \u00fcksikasjad. Kasutajanime ja salas\u00f5na m\u00e4\u00e4ramine on valikuline kuid v\u00f5imaldab rohkemate sidumisfunktsioonide toetamist. Teisest k\u00fcljest v\u00f5ib volitatud \u00fchenduse kasutamine p\u00f5hjustada probleeme seadme veebiliidese ligip\u00e4\u00e4suga v\u00e4ljastpoolt Home assistant'i kui sidumine on aktiivne ja vastupidi.", + "description": "Sisesta seadmele juurdep\u00e4\u00e4su \u00fcksikasjad.", "title": "Huawei LTE seadistamine" } } @@ -35,7 +35,8 @@ "name": "Teavitusteenuse nimi (muudatus n\u00f5uab taask\u00e4ivitamist)", "recipient": "SMS teavituse saajad", "track_new_devices": "Uute seadmete j\u00e4lgimine", - "track_wired_clients": "J\u00e4lgi juhtmega v\u00f5rgukliente" + "track_wired_clients": "J\u00e4lgi juhtmega v\u00f5rgukliente", + "unauthenticated_mode": "Tuvastuseta re\u017eiim (muutmine n\u00f5uab taaslaadimist)" } } } diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index efb441f8972..38ee2117b9d 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistanta, gdy integracja jest aktywna.", + "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia.", "title": "Konfiguracja Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia", - "track_wired_clients": "\u015aled\u017a klient\u00f3w sieci przewodowej" + "track_wired_clients": "\u015aled\u017a klient\u00f3w sieci przewodowej", + "unauthenticated_mode": "Tryb nieuwierzytelniony (zmiana wymaga prze\u0142adowania)" } } } diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json index 4453c7d227e..3bfc07a5fe1 100644 --- a/homeassistant/components/samsungtv/translations/cs.json +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -5,9 +5,14 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "auth_missing": "Home Assistant nem\u00e1 opr\u00e1vn\u011bn\u00ed k p\u0159ipojen\u00ed k t\u00e9to televizi Samsung. Zkontrolujte nastaven\u00ed sv\u00e9 televize a povolte Home Assistant.", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "not_supported": "Tato televize Samsung nen\u00ed aktu\u00e1ln\u011b podporov\u00e1na." + "not_supported": "Tato televize Samsung nen\u00ed aktu\u00e1ln\u011b podporov\u00e1na.", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, - "flow_title": "Samsung TV: {model}", + "error": { + "auth_missing": "Home Assistant nem\u00e1 opr\u00e1vn\u011bn\u00ed k p\u0159ipojen\u00ed k t\u00e9to televizi Samsung. Zkontrolujte nastaven\u00ed sv\u00e9 televize a povolte Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed.", diff --git a/homeassistant/components/sonos/translations/ca.json b/homeassistant/components/sonos/translations/ca.json index 4f9995c6c11..f0dbf0e2f0c 100644 --- a/homeassistant/components/sonos/translations/ca.json +++ b/homeassistant/components/sonos/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_sonos_device": "El dispositiu descobert no \u00e9s un dispositiu Sonos", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "step": { diff --git a/homeassistant/components/sonos/translations/et.json b/homeassistant/components/sonos/translations/et.json index 75f24b9b171..d330cd76cb0 100644 --- a/homeassistant/components/sonos/translations/et.json +++ b/homeassistant/components/sonos/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "V\u00f5rgust ei leitud seadmeid", + "not_sonos_device": "Avastatud seade ei ole Sonose seade", "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks sidumine." }, "step": { diff --git a/homeassistant/components/sonos/translations/nl.json b/homeassistant/components/sonos/translations/nl.json index 42298f0b4f7..2a8d5a7c56e 100644 --- a/homeassistant/components/sonos/translations/nl.json +++ b/homeassistant/components/sonos/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_sonos_device": "Ontdekt apparaat is geen Sonos-apparaat", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { diff --git a/homeassistant/components/sonos/translations/pl.json b/homeassistant/components/sonos/translations/pl.json index a8ee3fa57ac..67f9241f222 100644 --- a/homeassistant/components/sonos/translations/pl.json +++ b/homeassistant/components/sonos/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_sonos_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Sonos", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "step": { diff --git a/homeassistant/components/sonos/translations/ru.json b/homeassistant/components/sonos/translations/ru.json index f0b6ca6b6bf..b5e9d9b35d6 100644 --- a/homeassistant/components/sonos/translations/ru.json +++ b/homeassistant/components/sonos/translations/ru.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "not_sonos_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 Sonos.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "step": { diff --git a/homeassistant/components/sonos/translations/zh-Hant.json b/homeassistant/components/sonos/translations/zh-Hant.json index 31a9d3ce950..08434f16c15 100644 --- a/homeassistant/components/sonos/translations/zh-Hant.json +++ b/homeassistant/components/sonos/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_sonos_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Sonos \u88dd\u7f6e", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { diff --git a/homeassistant/components/syncthru/translations/cs.json b/homeassistant/components/syncthru/translations/cs.json index d34668146a3..2c9fbda88f0 100644 --- a/homeassistant/components/syncthru/translations/cs.json +++ b/homeassistant/components/syncthru/translations/cs.json @@ -6,7 +6,7 @@ "error": { "invalid_url": "Neplatn\u00e1 URL adresa" }, - "flow_title": "Tisk\u00e1rna Samsung SyncThru: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index d487ea8b902..cc1e03a40ad 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Configura el valor del par\u00e0metre {subtype}", + "node_status": "Estat del node", + "value": "Valor actual d'un valor Z-Wave" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 434a39e61d7..0439ec7c245 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Seadeparameeteri {subtype} v\u00e4\u00e4rtus", + "node_status": "S\u00f5lme olek", + "value": "Z-Wave Value praegune v\u00e4\u00e4rtus" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index c8b7b8b0ed9..6d6141e1238 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Warto\u015b\u0107 parametru jest {subtype}", + "node_status": "Stan w\u0119z\u0142a", + "value": "Aktualna warto\u015b\u0107 warto\u015bci Z-Wave" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", From f13a15f2a647fdde8c113eb150a069c0e0756ca1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 14 Jul 2021 02:56:10 -0400 Subject: [PATCH 278/818] Make zwave_js value updated event logic more performant (#52997) --- homeassistant/components/zwave_js/__init__.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2bb25000504..502510539cf 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -141,7 +141,7 @@ async def async_setup_entry( # noqa: C901 if device.id not in registered_unique_ids: registered_unique_ids[device.id] = defaultdict(set) - value_updates_disc_info = [] + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): @@ -173,7 +173,7 @@ async def async_setup_entry( # noqa: C901 # Capture discovery info for values we want to watch for updates if disc_info.assumed_state: - value_updates_disc_info.append(disc_info) + value_updates_disc_info[disc_info.primary_value.value_id] = disc_info # add listener for value updated events if necessary if value_updates_disc_info: @@ -313,19 +313,14 @@ async def async_setup_entry( # noqa: C901 @callback def async_on_value_updated( - value_updates_disc_info: list[ZwaveDiscoveryInfo], value: Value + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" - # Get the discovery info for the value that was updated. If we can't - # find the discovery info, we don't need to fire an event - try: - disc_info = next( - disc_info - for disc_info in value_updates_disc_info - if disc_info.primary_value.value_id == value.value_id - ) - except StopIteration: + # Get the discovery info for the value that was updated. If there is + # no discovery info for this value, we don't need to fire an event + if value.value_id not in value_updates_disc_info: return + disc_info = value_updates_disc_info[value.value_id] device = dev_reg.async_get_device({get_device_id(client, value.node)}) From 88fb30af110d227b351163678c92198ae3031b64 Mon Sep 17 00:00:00 2001 From: Doug Hoffman Date: Wed, 14 Jul 2021 04:45:47 -0400 Subject: [PATCH 279/818] Bump pyatv to 0.8.1 (#52849) * Bump pyatv to 0.8.1 * Update apple_tv tests for new create_session location * Update test_user_adds_unusable_device to try device with no services pyatv >=0.8.0 considers AirPlay a valid service and no longer fails under the previous conditions. --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/apple_tv/conftest.py | 13 +++++-------- tests/components/apple_tv/test_config_flow.py | 6 +++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 963cbb9be33..d4eb322f4d7 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.7.7"], + "requirements": ["pyatv==0.8.1"], "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "after_dependencies": ["discovery"], "codeowners": ["@postlund"], diff --git a/requirements_all.txt b/requirements_all.txt index db305130f24..da020c665f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1316,7 +1316,7 @@ pyatmo==5.2.0 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.7 +pyatv==0.8.1 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f4e2ab5855..87a084a3ece 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -741,7 +741,7 @@ pyatag==0.3.5.3 pyatmo==5.2.0 # homeassistant.components.apple_tv -pyatv==0.7.7 +pyatv==0.8.1 # homeassistant.components.blackbird pyblackbird==0.5 diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index db543007fb2..f07fa7d70bb 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -2,7 +2,8 @@ from unittest.mock import patch -from pyatv import conf, net +from pyatv import conf +from pyatv.support.http import create_session import pytest from .common import MockPairingHandler, create_conf @@ -39,7 +40,7 @@ def pairing(): async def _pair(config, protocol, loop, session=None, **kwargs): handler = MockPairingHandler( - await net.create_session(session), config.get_service(protocol) + await create_session(session), config.get_service(protocol) ) handler.always_fail = mock_pair.always_fail return handler @@ -121,11 +122,7 @@ def dmap_device_with_credentials(mock_scan): @pytest.fixture -def airplay_device(mock_scan): +def device_with_no_services(mock_scan): """Mock pyatv.scan.""" - mock_scan.result.append( - create_conf( - "127.0.0.1", "AirPlay Device", conf.AirPlayService("airplayid", port=7777) - ) - ) + mock_scan.result.append(create_conf("127.0.0.1", "Invalid Device")) yield mock_scan diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 615a1f404f5..45edaa36251 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -236,15 +236,15 @@ async def test_user_adds_existing_device(hass, mrp_device): assert result2["errors"] == {"base": "already_configured"} -async def test_user_adds_unusable_device(hass, airplay_device): - """Test that it is not possible to add pure AirPlay device.""" +async def test_user_adds_unusable_device(hass, device_with_no_services): + """Test that it is not possible to add device with no services.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"device_input": "AirPlay Device"}, + {"device_input": "Invalid Device"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "no_usable_service"} From 03dd2e326c575194e848cc3122257656cdb4464f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Jul 2021 10:54:52 +0200 Subject: [PATCH 280/818] Remove flume for allowed ignore coverage violations (#53001) --- script/hassfest/coverage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 1a4b1fbf8ba..a8570283f57 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -28,7 +28,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("elkm1", "config_flow.py"), ("elkm1", "scene.py"), ("fibaro", "scene.py"), - ("flume", "config_flow.py"), ("hangouts", "config_flow.py"), ("harmony", "config_flow.py"), ("hisense_aehw4a1", "config_flow.py"), From 2c3f3d7bdac266171ed310d1e0a593bddba91705 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Jul 2021 10:55:06 +0200 Subject: [PATCH 281/818] Remove defunct Weather Underground integration (#52999) --- .../components/wunderground/__init__.py | 1 - .../components/wunderground/manifest.json | 7 - .../components/wunderground/sensor.py | 1282 ----------------- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - tests/components/wunderground/__init__.py | 1 - tests/components/wunderground/test_sensor.py | 188 --- tests/fixtures/wunderground-error.json | 11 - tests/fixtures/wunderground-invalid.json | 18 - tests/fixtures/wunderground-valid.json | 90 -- 10 files changed, 1602 deletions(-) delete mode 100644 homeassistant/components/wunderground/__init__.py delete mode 100644 homeassistant/components/wunderground/manifest.json delete mode 100644 homeassistant/components/wunderground/sensor.py delete mode 100644 tests/components/wunderground/__init__.py delete mode 100644 tests/components/wunderground/test_sensor.py delete mode 100644 tests/fixtures/wunderground-error.json delete mode 100644 tests/fixtures/wunderground-invalid.json delete mode 100644 tests/fixtures/wunderground-valid.json diff --git a/homeassistant/components/wunderground/__init__.py b/homeassistant/components/wunderground/__init__.py deleted file mode 100644 index faed41fdbea..00000000000 --- a/homeassistant/components/wunderground/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The wunderground component.""" diff --git a/homeassistant/components/wunderground/manifest.json b/homeassistant/components/wunderground/manifest.json deleted file mode 100644 index b932d9ac403..00000000000 --- a/homeassistant/components/wunderground/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "wunderground", - "name": "Weather Underground (WUnderground)", - "documentation": "https://www.home-assistant.io/integrations/wunderground", - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py deleted file mode 100644 index 887e2264a70..00000000000 --- a/homeassistant/components/wunderground/sensor.py +++ /dev/null @@ -1,1282 +0,0 @@ -"""Support for WUnderground weather service.""" -from __future__ import annotations - -import asyncio -from datetime import timedelta -import logging -import re -from typing import Any, Callable - -import aiohttp -import async_timeout -import voluptuous as vol - -from homeassistant.components import sensor -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, - DEGREE, - IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_FEET, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_MILLIMETERS, - PERCENTAGE, - PRESSURE_INHG, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle - -_RESOURCE = "http://api.wunderground.com/api/{}/{}/{}/q/" -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Data provided by the WUnderground weather service" - -CONF_PWS_ID = "pws_id" -CONF_LANG = "lang" - -DEFAULT_LANG = "EN" - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - - -# Helper classes for declaring sensor configurations - - -class WUSensorConfig: - """WU Sensor Configuration. - - defines basic HA properties of the weather sensor and - stores callbacks that can parse sensor values out of - the json data received by WU API. - """ - - def __init__( - self, - friendly_name: str | Callable, - feature: str, - value: Callable[[WUndergroundData], Any], - unit_of_measurement: str | None = None, - entity_picture=None, - icon: str = "mdi:gauge", - extra_state_attributes=None, - device_class=None, - ) -> None: - """Initialize sensor configuration. - - :param friendly_name: Friendly name - :param feature: WU feature. See: - https://www.wunderground.com/weather/api/d/docs?d=data/index - :param value: callback that extracts desired value from WUndergroundData object - :param unit_of_measurement: unit of measurement - :param entity_picture: value or callback returning URL of entity picture - :param icon: icon name or URL - :param extra_state_attributes: dictionary of attributes, or callable that returns it - """ - self.friendly_name = friendly_name - self.unit_of_measurement = unit_of_measurement - self.feature = feature - self.value = value - self.entity_picture = entity_picture - self.icon = icon - self.extra_state_attributes = extra_state_attributes or {} - self.device_class = device_class - - -class WUCurrentConditionsSensorConfig(WUSensorConfig): - """Helper for defining sensor configurations for current conditions.""" - - def __init__( - self, - friendly_name: str | Callable, - field: str, - icon: str | None = "mdi:gauge", - unit_of_measurement: str | None = None, - device_class=None, - ) -> None: - """Initialize current conditions sensor configuration. - - :param friendly_name: Friendly name of sensor - :field: Field name in the "current_observation" dictionary. - :icon: icon name or URL, if None sensor will use current weather symbol - :unit_of_measurement: unit of measurement - """ - super().__init__( - friendly_name, - "conditions", - value=lambda wu: wu.data["current_observation"][field], - icon=icon, - unit_of_measurement=unit_of_measurement, - entity_picture=lambda wu: wu.data["current_observation"]["icon_url"] - if icon is None - else None, - extra_state_attributes={ - "date": lambda wu: wu.data["current_observation"]["observation_time"] - }, - device_class=device_class, - ) - - -class WUDailyTextForecastSensorConfig(WUSensorConfig): - """Helper for defining sensor configurations for daily text forecasts.""" - - def __init__( - self, period: int, field: str, unit_of_measurement: str | None = None - ) -> None: - """Initialize daily text forecast sensor configuration. - - :param period: forecast period number - :param field: field name to use as value - :param unit_of_measurement: unit of measurement - """ - super().__init__( - friendly_name=lambda wu: wu.data["forecast"]["txt_forecast"]["forecastday"][ - period - ]["title"], - feature="forecast", - value=lambda wu: wu.data["forecast"]["txt_forecast"]["forecastday"][period][ - field - ], - entity_picture=lambda wu: wu.data["forecast"]["txt_forecast"][ - "forecastday" - ][period]["icon_url"], - unit_of_measurement=unit_of_measurement, - extra_state_attributes={ - "date": lambda wu: wu.data["forecast"]["txt_forecast"]["date"] - }, - ) - - -class WUDailySimpleForecastSensorConfig(WUSensorConfig): - """Helper for defining sensor configurations for daily simpleforecasts.""" - - def __init__( - self, - friendly_name: str, - period: int, - field: str, - wu_unit: str | None = None, - ha_unit: str | None = None, - icon=None, - device_class=None, - ) -> None: - """Initialize daily simple forecast sensor configuration. - - :param friendly_name: friendly_name of the sensor - :param period: forecast period number - :param field: field name to use as value - :param wu_unit: "fahrenheit", "celsius", "degrees" etc. see the example json at: - https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 - :param ha_unit: corresponding unit in Home Assistant - """ - super().__init__( - friendly_name=friendly_name, - feature="forecast", - value=( - lambda wu: wu.data["forecast"]["simpleforecast"]["forecastday"][period][ - field - ][wu_unit] - ) - if wu_unit - else ( - lambda wu: wu.data["forecast"]["simpleforecast"]["forecastday"][period][ - field - ] - ), - unit_of_measurement=ha_unit, - entity_picture=lambda wu: wu.data["forecast"]["simpleforecast"][ - "forecastday" - ][period]["icon_url"] - if not icon - else None, - icon=icon, - extra_state_attributes={ - "date": lambda wu: wu.data["forecast"]["simpleforecast"]["forecastday"][ - period - ]["date"]["pretty"] - }, - device_class=device_class, - ) - - -class WUHourlyForecastSensorConfig(WUSensorConfig): - """Helper for defining sensor configurations for hourly text forecasts.""" - - def __init__(self, period: int, field: int) -> None: - """Initialize hourly forecast sensor configuration. - - :param period: forecast period number - :param field: field name to use as value - """ - super().__init__( - friendly_name=lambda wu: ( - f"{wu.data['hourly_forecast'][period]['FCTTIME']['weekday_name_abbrev']} " - f"{wu.data['hourly_forecast'][period]['FCTTIME']['civil']}" - ), - feature="hourly", - value=lambda wu: wu.data["hourly_forecast"][period][field], - entity_picture=lambda wu: wu.data["hourly_forecast"][period]["icon_url"], - extra_state_attributes={ - "temp_c": lambda wu: wu.data["hourly_forecast"][period]["temp"][ - "metric" - ], - "temp_f": lambda wu: wu.data["hourly_forecast"][period]["temp"][ - "english" - ], - "dewpoint_c": lambda wu: wu.data["hourly_forecast"][period]["dewpoint"][ - "metric" - ], - "dewpoint_f": lambda wu: wu.data["hourly_forecast"][period]["dewpoint"][ - "english" - ], - "precip_prop": lambda wu: wu.data["hourly_forecast"][period]["pop"], - "sky": lambda wu: wu.data["hourly_forecast"][period]["sky"], - "precip_mm": lambda wu: wu.data["hourly_forecast"][period]["qpf"][ - "metric" - ], - "precip_in": lambda wu: wu.data["hourly_forecast"][period]["qpf"][ - "english" - ], - "humidity": lambda wu: wu.data["hourly_forecast"][period]["humidity"], - "wind_kph": lambda wu: wu.data["hourly_forecast"][period]["wspd"][ - "metric" - ], - "wind_mph": lambda wu: wu.data["hourly_forecast"][period]["wspd"][ - "english" - ], - "pressure_mb": lambda wu: wu.data["hourly_forecast"][period]["mslp"][ - "metric" - ], - "pressure_inHg": lambda wu: wu.data["hourly_forecast"][period]["mslp"][ - "english" - ], - "date": lambda wu: wu.data["hourly_forecast"][period]["FCTTIME"][ - "pretty" - ], - }, - ) - - -class WUAlmanacSensorConfig(WUSensorConfig): - """Helper for defining field configurations for almanac sensors.""" - - def __init__( - self, - friendly_name: str | Callable, - field: str, - value_type: str, - wu_unit: str, - unit_of_measurement: str, - icon: str, - device_class=None, - ) -> None: - """Initialize almanac sensor configuration. - - :param friendly_name: Friendly name - :param field: value name returned in 'almanac' dict as returned by the WU API - :param value_type: "record" or "normal" - :param wu_unit: unit name in WU API - :param unit_of_measurement: unit of measurement - :param icon: icon name or URL - """ - super().__init__( - friendly_name=friendly_name, - feature="almanac", - value=lambda wu: wu.data["almanac"][field][value_type][wu_unit], - unit_of_measurement=unit_of_measurement, - icon=icon, - device_class="temperature", - ) - - -class WUAlertsSensorConfig(WUSensorConfig): - """Helper for defining field configuration for alerts.""" - - def __init__(self, friendly_name: str | Callable) -> None: - """Initialiize alerts sensor configuration. - - :param friendly_name: Friendly name - """ - super().__init__( - friendly_name=friendly_name, - feature="alerts", - value=lambda wu: len(wu.data["alerts"]), - icon=lambda wu: "mdi:alert-circle-outline" - if wu.data["alerts"] - else "mdi:check-circle-outline", - extra_state_attributes=self._get_attributes, - ) - - @staticmethod - def _get_attributes(rest): - - attrs = {} - - if "alerts" not in rest.data: - return attrs - - alerts = rest.data["alerts"] - multiple_alerts = len(alerts) > 1 - for data in alerts: - for alert in ALERTS_ATTRS: - if data[alert]: - if multiple_alerts: - dkey = f"{alert.capitalize()}_{data['type']}" - else: - dkey = alert.capitalize() - attrs[dkey] = data[alert] - return attrs - - -# Declaration of supported WU sensors -# (see above helper classes for argument explanation) - -SENSOR_TYPES = { - "alerts": WUAlertsSensorConfig("Alerts"), - "dewpoint_c": WUCurrentConditionsSensorConfig( - "Dewpoint", "dewpoint_c", "mdi:water", TEMP_CELSIUS - ), - "dewpoint_f": WUCurrentConditionsSensorConfig( - "Dewpoint", "dewpoint_f", "mdi:water", TEMP_FAHRENHEIT - ), - "dewpoint_string": WUCurrentConditionsSensorConfig( - "Dewpoint Summary", "dewpoint_string", "mdi:water" - ), - "feelslike_c": WUCurrentConditionsSensorConfig( - "Feels Like", "feelslike_c", "mdi:thermometer", TEMP_CELSIUS - ), - "feelslike_f": WUCurrentConditionsSensorConfig( - "Feels Like", "feelslike_f", "mdi:thermometer", TEMP_FAHRENHEIT - ), - "feelslike_string": WUCurrentConditionsSensorConfig( - "Feels Like", "feelslike_string", "mdi:thermometer" - ), - "heat_index_c": WUCurrentConditionsSensorConfig( - "Heat index", "heat_index_c", "mdi:thermometer", TEMP_CELSIUS - ), - "heat_index_f": WUCurrentConditionsSensorConfig( - "Heat index", "heat_index_f", "mdi:thermometer", TEMP_FAHRENHEIT - ), - "heat_index_string": WUCurrentConditionsSensorConfig( - "Heat Index Summary", "heat_index_string", "mdi:thermometer" - ), - "elevation": WUSensorConfig( - "Elevation", - "conditions", - value=lambda wu: wu.data["current_observation"]["observation_location"][ - "elevation" - ].split()[0], - unit_of_measurement=LENGTH_FEET, - icon="mdi:elevation-rise", - ), - "location": WUSensorConfig( - "Location", - "conditions", - value=lambda wu: wu.data["current_observation"]["display_location"]["full"], - icon="mdi:map-marker", - ), - "observation_time": WUCurrentConditionsSensorConfig( - "Observation Time", "observation_time", "mdi:clock" - ), - "precip_1hr_in": WUCurrentConditionsSensorConfig( - "Precipitation 1hr", "precip_1hr_in", "mdi:umbrella", LENGTH_INCHES - ), - "precip_1hr_metric": WUCurrentConditionsSensorConfig( - "Precipitation 1hr", "precip_1hr_metric", "mdi:umbrella", LENGTH_MILLIMETERS - ), - "precip_1hr_string": WUCurrentConditionsSensorConfig( - "Precipitation 1hr", "precip_1hr_string", "mdi:umbrella" - ), - "precip_today_in": WUCurrentConditionsSensorConfig( - "Precipitation Today", "precip_today_in", "mdi:umbrella", LENGTH_INCHES - ), - "precip_today_metric": WUCurrentConditionsSensorConfig( - "Precipitation Today", "precip_today_metric", "mdi:umbrella", LENGTH_MILLIMETERS - ), - "precip_today_string": WUCurrentConditionsSensorConfig( - "Precipitation Today", "precip_today_string", "mdi:umbrella" - ), - "pressure_in": WUCurrentConditionsSensorConfig( - "Pressure", "pressure_in", "mdi:gauge", PRESSURE_INHG, device_class="pressure" - ), - "pressure_mb": WUCurrentConditionsSensorConfig( - "Pressure", "pressure_mb", "mdi:gauge", "mb", device_class="pressure" - ), - "pressure_trend": WUCurrentConditionsSensorConfig( - "Pressure Trend", "pressure_trend", "mdi:gauge", device_class="pressure" - ), - "relative_humidity": WUSensorConfig( - "Relative Humidity", - "conditions", - value=lambda wu: int(wu.data["current_observation"]["relative_humidity"][:-1]), - unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", - device_class="humidity", - ), - "station_id": WUCurrentConditionsSensorConfig( - "Station ID", "station_id", "mdi:home" - ), - "solarradiation": WUCurrentConditionsSensorConfig( - "Solar Radiation", - "solarradiation", - "mdi:weather-sunny", - IRRADIATION_WATTS_PER_SQUARE_METER, - ), - "temperature_string": WUCurrentConditionsSensorConfig( - "Temperature Summary", "temperature_string", "mdi:thermometer" - ), - "temp_c": WUCurrentConditionsSensorConfig( - "Temperature", - "temp_c", - "mdi:thermometer", - TEMP_CELSIUS, - device_class="temperature", - ), - "temp_f": WUCurrentConditionsSensorConfig( - "Temperature", - "temp_f", - "mdi:thermometer", - TEMP_FAHRENHEIT, - device_class="temperature", - ), - "UV": WUCurrentConditionsSensorConfig("UV", "UV", "mdi:sunglasses"), - "visibility_km": WUCurrentConditionsSensorConfig( - "Visibility (km)", "visibility_km", "mdi:eye", LENGTH_KILOMETERS - ), - "visibility_mi": WUCurrentConditionsSensorConfig( - "Visibility (miles)", "visibility_mi", "mdi:eye", LENGTH_MILES - ), - "weather": WUCurrentConditionsSensorConfig("Weather Summary", "weather", None), - "wind_degrees": WUCurrentConditionsSensorConfig( - "Wind Degrees", "wind_degrees", "mdi:weather-windy", DEGREE - ), - "wind_dir": WUCurrentConditionsSensorConfig( - "Wind Direction", "wind_dir", "mdi:weather-windy" - ), - "wind_gust_kph": WUCurrentConditionsSensorConfig( - "Wind Gust", "wind_gust_kph", "mdi:weather-windy", SPEED_KILOMETERS_PER_HOUR - ), - "wind_gust_mph": WUCurrentConditionsSensorConfig( - "Wind Gust", "wind_gust_mph", "mdi:weather-windy", SPEED_MILES_PER_HOUR - ), - "wind_kph": WUCurrentConditionsSensorConfig( - "Wind Speed", "wind_kph", "mdi:weather-windy", SPEED_KILOMETERS_PER_HOUR - ), - "wind_mph": WUCurrentConditionsSensorConfig( - "Wind Speed", "wind_mph", "mdi:weather-windy", SPEED_MILES_PER_HOUR - ), - "wind_string": WUCurrentConditionsSensorConfig( - "Wind Summary", "wind_string", "mdi:weather-windy" - ), - "temp_high_record_c": WUAlmanacSensorConfig( - lambda wu: ( - f"High Temperature Record " - f"({wu.data['almanac']['temp_high']['recordyear']})" - ), - "temp_high", - "record", - "C", - TEMP_CELSIUS, - "mdi:thermometer", - ), - "temp_high_record_f": WUAlmanacSensorConfig( - lambda wu: ( - f"High Temperature Record " - f"({wu.data['almanac']['temp_high']['recordyear']})" - ), - "temp_high", - "record", - "F", - TEMP_FAHRENHEIT, - "mdi:thermometer", - ), - "temp_low_record_c": WUAlmanacSensorConfig( - lambda wu: ( - f"Low Temperature Record " - f"({wu.data['almanac']['temp_low']['recordyear']})" - ), - "temp_low", - "record", - "C", - TEMP_CELSIUS, - "mdi:thermometer", - ), - "temp_low_record_f": WUAlmanacSensorConfig( - lambda wu: ( - f"Low Temperature Record " - f"({wu.data['almanac']['temp_low']['recordyear']})" - ), - "temp_low", - "record", - "F", - TEMP_FAHRENHEIT, - "mdi:thermometer", - ), - "temp_low_avg_c": WUAlmanacSensorConfig( - "Historic Average of Low Temperatures for Today", - "temp_low", - "normal", - "C", - TEMP_CELSIUS, - "mdi:thermometer", - ), - "temp_low_avg_f": WUAlmanacSensorConfig( - "Historic Average of Low Temperatures for Today", - "temp_low", - "normal", - "F", - TEMP_FAHRENHEIT, - "mdi:thermometer", - ), - "temp_high_avg_c": WUAlmanacSensorConfig( - "Historic Average of High Temperatures for Today", - "temp_high", - "normal", - "C", - TEMP_CELSIUS, - "mdi:thermometer", - ), - "temp_high_avg_f": WUAlmanacSensorConfig( - "Historic Average of High Temperatures for Today", - "temp_high", - "normal", - "F", - TEMP_FAHRENHEIT, - "mdi:thermometer", - ), - "weather_1d": WUDailyTextForecastSensorConfig(0, "fcttext"), - "weather_1d_metric": WUDailyTextForecastSensorConfig(0, "fcttext_metric"), - "weather_1n": WUDailyTextForecastSensorConfig(1, "fcttext"), - "weather_1n_metric": WUDailyTextForecastSensorConfig(1, "fcttext_metric"), - "weather_2d": WUDailyTextForecastSensorConfig(2, "fcttext"), - "weather_2d_metric": WUDailyTextForecastSensorConfig(2, "fcttext_metric"), - "weather_2n": WUDailyTextForecastSensorConfig(3, "fcttext"), - "weather_2n_metric": WUDailyTextForecastSensorConfig(3, "fcttext_metric"), - "weather_3d": WUDailyTextForecastSensorConfig(4, "fcttext"), - "weather_3d_metric": WUDailyTextForecastSensorConfig(4, "fcttext_metric"), - "weather_3n": WUDailyTextForecastSensorConfig(5, "fcttext"), - "weather_3n_metric": WUDailyTextForecastSensorConfig(5, "fcttext_metric"), - "weather_4d": WUDailyTextForecastSensorConfig(6, "fcttext"), - "weather_4d_metric": WUDailyTextForecastSensorConfig(6, "fcttext_metric"), - "weather_4n": WUDailyTextForecastSensorConfig(7, "fcttext"), - "weather_4n_metric": WUDailyTextForecastSensorConfig(7, "fcttext_metric"), - "weather_1h": WUHourlyForecastSensorConfig(0, "condition"), - "weather_2h": WUHourlyForecastSensorConfig(1, "condition"), - "weather_3h": WUHourlyForecastSensorConfig(2, "condition"), - "weather_4h": WUHourlyForecastSensorConfig(3, "condition"), - "weather_5h": WUHourlyForecastSensorConfig(4, "condition"), - "weather_6h": WUHourlyForecastSensorConfig(5, "condition"), - "weather_7h": WUHourlyForecastSensorConfig(6, "condition"), - "weather_8h": WUHourlyForecastSensorConfig(7, "condition"), - "weather_9h": WUHourlyForecastSensorConfig(8, "condition"), - "weather_10h": WUHourlyForecastSensorConfig(9, "condition"), - "weather_11h": WUHourlyForecastSensorConfig(10, "condition"), - "weather_12h": WUHourlyForecastSensorConfig(11, "condition"), - "weather_13h": WUHourlyForecastSensorConfig(12, "condition"), - "weather_14h": WUHourlyForecastSensorConfig(13, "condition"), - "weather_15h": WUHourlyForecastSensorConfig(14, "condition"), - "weather_16h": WUHourlyForecastSensorConfig(15, "condition"), - "weather_17h": WUHourlyForecastSensorConfig(16, "condition"), - "weather_18h": WUHourlyForecastSensorConfig(17, "condition"), - "weather_19h": WUHourlyForecastSensorConfig(18, "condition"), - "weather_20h": WUHourlyForecastSensorConfig(19, "condition"), - "weather_21h": WUHourlyForecastSensorConfig(20, "condition"), - "weather_22h": WUHourlyForecastSensorConfig(21, "condition"), - "weather_23h": WUHourlyForecastSensorConfig(22, "condition"), - "weather_24h": WUHourlyForecastSensorConfig(23, "condition"), - "weather_25h": WUHourlyForecastSensorConfig(24, "condition"), - "weather_26h": WUHourlyForecastSensorConfig(25, "condition"), - "weather_27h": WUHourlyForecastSensorConfig(26, "condition"), - "weather_28h": WUHourlyForecastSensorConfig(27, "condition"), - "weather_29h": WUHourlyForecastSensorConfig(28, "condition"), - "weather_30h": WUHourlyForecastSensorConfig(29, "condition"), - "weather_31h": WUHourlyForecastSensorConfig(30, "condition"), - "weather_32h": WUHourlyForecastSensorConfig(31, "condition"), - "weather_33h": WUHourlyForecastSensorConfig(32, "condition"), - "weather_34h": WUHourlyForecastSensorConfig(33, "condition"), - "weather_35h": WUHourlyForecastSensorConfig(34, "condition"), - "weather_36h": WUHourlyForecastSensorConfig(35, "condition"), - "temp_high_1d_c": WUDailySimpleForecastSensorConfig( - "High Temperature Today", - 0, - "high", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_2d_c": WUDailySimpleForecastSensorConfig( - "High Temperature Tomorrow", - 1, - "high", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_3d_c": WUDailySimpleForecastSensorConfig( - "High Temperature in 3 Days", - 2, - "high", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_4d_c": WUDailySimpleForecastSensorConfig( - "High Temperature in 4 Days", - 3, - "high", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_1d_f": WUDailySimpleForecastSensorConfig( - "High Temperature Today", - 0, - "high", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_2d_f": WUDailySimpleForecastSensorConfig( - "High Temperature Tomorrow", - 1, - "high", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_3d_f": WUDailySimpleForecastSensorConfig( - "High Temperature in 3 Days", - 2, - "high", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_high_4d_f": WUDailySimpleForecastSensorConfig( - "High Temperature in 4 Days", - 3, - "high", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_1d_c": WUDailySimpleForecastSensorConfig( - "Low Temperature Today", - 0, - "low", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_2d_c": WUDailySimpleForecastSensorConfig( - "Low Temperature Tomorrow", - 1, - "low", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_3d_c": WUDailySimpleForecastSensorConfig( - "Low Temperature in 3 Days", - 2, - "low", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_4d_c": WUDailySimpleForecastSensorConfig( - "Low Temperature in 4 Days", - 3, - "low", - "celsius", - TEMP_CELSIUS, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_1d_f": WUDailySimpleForecastSensorConfig( - "Low Temperature Today", - 0, - "low", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_2d_f": WUDailySimpleForecastSensorConfig( - "Low Temperature Tomorrow", - 1, - "low", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_3d_f": WUDailySimpleForecastSensorConfig( - "Low Temperature in 3 Days", - 2, - "low", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "temp_low_4d_f": WUDailySimpleForecastSensorConfig( - "Low Temperature in 4 Days", - 3, - "low", - "fahrenheit", - TEMP_FAHRENHEIT, - "mdi:thermometer", - device_class="temperature", - ), - "wind_gust_1d_kph": WUDailySimpleForecastSensorConfig( - "Max. Wind Today", - 0, - "maxwind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_2d_kph": WUDailySimpleForecastSensorConfig( - "Max. Wind Tomorrow", - 1, - "maxwind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_3d_kph": WUDailySimpleForecastSensorConfig( - "Max. Wind in 3 Days", - 2, - "maxwind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_4d_kph": WUDailySimpleForecastSensorConfig( - "Max. Wind in 4 Days", - 3, - "maxwind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_1d_mph": WUDailySimpleForecastSensorConfig( - "Max. Wind Today", - 0, - "maxwind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_2d_mph": WUDailySimpleForecastSensorConfig( - "Max. Wind Tomorrow", - 1, - "maxwind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_3d_mph": WUDailySimpleForecastSensorConfig( - "Max. Wind in 3 Days", - 2, - "maxwind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_gust_4d_mph": WUDailySimpleForecastSensorConfig( - "Max. Wind in 4 Days", - 3, - "maxwind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_1d_kph": WUDailySimpleForecastSensorConfig( - "Avg. Wind Today", - 0, - "avewind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_2d_kph": WUDailySimpleForecastSensorConfig( - "Avg. Wind Tomorrow", - 1, - "avewind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_3d_kph": WUDailySimpleForecastSensorConfig( - "Avg. Wind in 3 Days", - 2, - "avewind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_4d_kph": WUDailySimpleForecastSensorConfig( - "Avg. Wind in 4 Days", - 3, - "avewind", - SPEED_KILOMETERS_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - ), - "wind_1d_mph": WUDailySimpleForecastSensorConfig( - "Avg. Wind Today", - 0, - "avewind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_2d_mph": WUDailySimpleForecastSensorConfig( - "Avg. Wind Tomorrow", - 1, - "avewind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_3d_mph": WUDailySimpleForecastSensorConfig( - "Avg. Wind in 3 Days", - 2, - "avewind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "wind_4d_mph": WUDailySimpleForecastSensorConfig( - "Avg. Wind in 4 Days", - 3, - "avewind", - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ), - "precip_1d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity Today", - 0, - "qpf_allday", - LENGTH_MILLIMETERS, - LENGTH_MILLIMETERS, - "mdi:umbrella", - ), - "precip_2d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity Tomorrow", - 1, - "qpf_allday", - LENGTH_MILLIMETERS, - LENGTH_MILLIMETERS, - "mdi:umbrella", - ), - "precip_3d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity in 3 Days", - 2, - "qpf_allday", - LENGTH_MILLIMETERS, - LENGTH_MILLIMETERS, - "mdi:umbrella", - ), - "precip_4d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity in 4 Days", - 3, - "qpf_allday", - LENGTH_MILLIMETERS, - LENGTH_MILLIMETERS, - "mdi:umbrella", - ), - "precip_1d_in": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity Today", - 0, - "qpf_allday", - "in", - LENGTH_INCHES, - "mdi:umbrella", - ), - "precip_2d_in": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity Tomorrow", - 1, - "qpf_allday", - "in", - LENGTH_INCHES, - "mdi:umbrella", - ), - "precip_3d_in": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity in 3 Days", - 2, - "qpf_allday", - "in", - LENGTH_INCHES, - "mdi:umbrella", - ), - "precip_4d_in": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity in 4 Days", - 3, - "qpf_allday", - "in", - LENGTH_INCHES, - "mdi:umbrella", - ), - "precip_1d": WUDailySimpleForecastSensorConfig( - "Precipitation Probability Today", - 0, - "pop", - None, - PERCENTAGE, - "mdi:umbrella", - ), - "precip_2d": WUDailySimpleForecastSensorConfig( - "Precipitation Probability Tomorrow", - 1, - "pop", - None, - PERCENTAGE, - "mdi:umbrella", - ), - "precip_3d": WUDailySimpleForecastSensorConfig( - "Precipitation Probability in 3 Days", - 2, - "pop", - None, - PERCENTAGE, - "mdi:umbrella", - ), - "precip_4d": WUDailySimpleForecastSensorConfig( - "Precipitation Probability in 4 Days", - 3, - "pop", - None, - PERCENTAGE, - "mdi:umbrella", - ), -} - -# Alert Attributes -ALERTS_ATTRS = ["date", "description", "expires", "message"] - -# Language Supported Codes -LANG_CODES = [ - "AF", - "AL", - "AR", - "HY", - "AZ", - "EU", - "BY", - "BU", - "LI", - "MY", - "CA", - "CN", - "TW", - "CR", - "CZ", - "DK", - "DV", - "NL", - "EN", - "EO", - "ET", - "FA", - "FI", - "FR", - "FC", - "GZ", - "DL", - "KA", - "GR", - "GU", - "HT", - "IL", - "HI", - "HU", - "IS", - "IO", - "ID", - "IR", - "IT", - "JP", - "JW", - "KM", - "KR", - "KU", - "LA", - "LV", - "LT", - "ND", - "MK", - "MT", - "GM", - "MI", - "MR", - "MN", - "NO", - "OC", - "PS", - "GN", - "PL", - "BR", - "PA", - "RO", - "RU", - "SR", - "SK", - "SL", - "SP", - "SI", - "SW", - "CH", - "TL", - "TT", - "TH", - "TR", - "TK", - "UA", - "UZ", - "VU", - "CY", - "SN", - "JI", - "YI", -] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_PWS_ID): cv.string, - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.All(vol.In(LANG_CODES)), - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - vol.Required(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)] - ), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None -): - """Set up the WUnderground sensor.""" - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - pws_id = config.get(CONF_PWS_ID) - - rest = WUndergroundData( - hass, - config.get(CONF_API_KEY), - pws_id, - config.get(CONF_LANG), - latitude, - longitude, - ) - - if pws_id is None: - unique_id_base = f"@{longitude:06f},{latitude:06f}" - else: - # Manually specified weather station, use that for unique_id - unique_id_base = pws_id - sensors = [] - for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable, unique_id_base)) - - await rest.async_update() - if not rest.data: - raise PlatformNotReady - - async_add_entities(sensors, True) - - -class WUndergroundSensor(SensorEntity): - """Implementing the WUnderground sensor.""" - - def __init__( - self, hass: HomeAssistant, rest, condition, unique_id_base: str - ) -> None: - """Initialize the sensor.""" - self.rest = rest - self._condition = condition - self._state = None - self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._icon = None - self._entity_picture = None - self._unit_of_measurement = self._cfg_expand("unit_of_measurement") - self.rest.request_feature(SENSOR_TYPES[condition].feature) - # This is only the suggested entity id, it might get changed by - # the entity registry later. - self.entity_id = sensor.ENTITY_ID_FORMAT.format(f"pws_{condition}") - self._unique_id = f"{unique_id_base},{condition}" - self._device_class = self._cfg_expand("device_class") - - def _cfg_expand(self, what, default=None): - """Parse and return sensor data.""" - cfg = SENSOR_TYPES[self._condition] - val = getattr(cfg, what) - if not callable(val): - return val - try: - val = val(self.rest) - except (KeyError, IndexError, TypeError, ValueError) as err: - _LOGGER.warning( - "Failed to expand cfg from WU API. Condition: %s Attr: %s Error: %s", - self._condition, - what, - repr(err), - ) - val = default - - return val - - def _update_attrs(self): - """Parse and update device state attributes.""" - attrs = self._cfg_expand("extra_state_attributes", {}) - - for (attr, callback) in attrs.items(): - if callable(callback): - try: - self._attributes[attr] = callback(self.rest) - except (KeyError, IndexError, TypeError, ValueError) as err: - _LOGGER.warning( - "Failed to update attrs from WU API." - " Condition: %s Attr: %s Error: %s", - self._condition, - attr, - repr(err), - ) - else: - self._attributes[attr] = callback - - @property - def name(self): - """Return the name of the sensor.""" - return self._cfg_expand("friendly_name") - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Return icon.""" - return self._icon - - @property - def entity_picture(self): - """Return the entity picture.""" - return self._entity_picture - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return self._unit_of_measurement - - @property - def device_class(self): - """Return the units of measurement.""" - return self._device_class - - async def async_update(self): - """Update current conditions.""" - await self.rest.async_update() - - if not self.rest.data: - # no data, return - return - - self._state = self._cfg_expand("value") - self._update_attrs() - self._icon = self._cfg_expand("icon", super().icon) - url = self._cfg_expand("entity_picture") - if isinstance(url, str): - self._entity_picture = re.sub( - r"^http://", "https://", url, flags=re.IGNORECASE - ) - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - -class WUndergroundData: - """Get data from WUnderground.""" - - def __init__(self, hass, api_key, pws_id, lang, latitude, longitude): - """Initialize the data object.""" - self._hass = hass - self._api_key = api_key - self._pws_id = pws_id - self._lang = f"lang:{lang}" - self._latitude = latitude - self._longitude = longitude - self._features = set() - self.data = None - self._session = async_get_clientsession(self._hass) - - def request_feature(self, feature): - """Register feature to be fetched from WU API.""" - self._features.add(feature) - - def _build_url(self, baseurl=_RESOURCE): - url = baseurl.format( - self._api_key, "/".join(sorted(self._features)), self._lang - ) - if self._pws_id: - url = f"{url}pws:{self._pws_id}" - else: - url = f"{url}{self._latitude},{self._longitude}" - - return f"{url}.json" - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from WUnderground.""" - try: - with async_timeout.timeout(10): - response = await self._session.get(self._build_url()) - result = await response.json() - if "error" in result["response"]: - raise ValueError(result["response"]["error"]["description"]) - self.data = result - except ValueError as err: - _LOGGER.error("Check WUnderground API %s", err.args) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - _LOGGER.error("Error fetching WUnderground data: %s", repr(err)) diff --git a/mypy.ini b/mypy.ini index 03fc0781027..1479bb7c700 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1626,9 +1626,6 @@ ignore_errors = true [mypy-homeassistant.components.withings.*] ignore_errors = true -[mypy-homeassistant.components.wunderground.*] -ignore_errors = true - [mypy-homeassistant.components.xbox.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d28f56bdea4..013e52c0327 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -204,7 +204,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.wemo.*", "homeassistant.components.wink.*", "homeassistant.components.withings.*", - "homeassistant.components.wunderground.*", "homeassistant.components.xbox.*", "homeassistant.components.xiaomi_aqara.*", "homeassistant.components.xiaomi_miio.*", diff --git a/tests/components/wunderground/__init__.py b/tests/components/wunderground/__init__.py deleted file mode 100644 index d3f839a35f6..00000000000 --- a/tests/components/wunderground/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the wunderground component.""" diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py deleted file mode 100644 index 8709f5b6a46..00000000000 --- a/tests/components/wunderground/test_sensor.py +++ /dev/null @@ -1,188 +0,0 @@ -"""The tests for the WUnderground platform.""" -import aiohttp -from pytest import raises - -import homeassistant.components.wunderground.sensor as wunderground -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - LENGTH_INCHES, - STATE_UNKNOWN, - TEMP_CELSIUS, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component, load_fixture - -VALID_CONFIG_PWS = { - "platform": "wunderground", - "api_key": "foo", - "pws_id": "bar", - "monitored_conditions": [ - "weather", - "feelslike_c", - "alerts", - "elevation", - "location", - ], -} - -VALID_CONFIG = { - "platform": "wunderground", - "api_key": "foo", - "lang": "EN", - "monitored_conditions": [ - "weather", - "feelslike_c", - "alerts", - "elevation", - "location", - "weather_1d_metric", - "precip_1d_in", - ], -} - -INVALID_CONFIG = { - "platform": "wunderground", - "api_key": "BOB", - "pws_id": "bar", - "lang": "foo", - "monitored_conditions": ["weather", "feelslike_c", "alerts"], -} - -URL = ( - "http://api.wunderground.com/api/foo/alerts/conditions/forecast/lang" - ":EN/q/32.87336,-117.22743.json" -) -PWS_URL = "http://api.wunderground.com/api/foo/alerts/conditions/lang:EN/q/pws:bar.json" -INVALID_URL = ( - "http://api.wunderground.com/api/BOB/alerts/conditions/lang:foo/q/pws:bar.json" -) - - -async def test_setup(hass, aioclient_mock): - """Test that the component is loaded.""" - aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) - - with assert_setup_component(1, "sensor"): - await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) - await hass.async_block_till_done() - - -async def test_setup_pws(hass, aioclient_mock): - """Test that the component is loaded with PWS id.""" - aioclient_mock.get(PWS_URL, text=load_fixture("wunderground-valid.json")) - - with assert_setup_component(1, "sensor"): - await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG_PWS}) - - -async def test_setup_invalid(hass, aioclient_mock): - """Test that the component is not loaded with invalid config.""" - aioclient_mock.get(INVALID_URL, text=load_fixture("wunderground-error.json")) - - with assert_setup_component(0, "sensor"): - await async_setup_component(hass, "sensor", {"sensor": INVALID_CONFIG}) - - -async def test_sensor(hass, aioclient_mock): - """Test the WUnderground sensor class and methods.""" - aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) - - await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) - await hass.async_block_till_done() - - state = hass.states.get("sensor.pws_weather") - assert state.state == "Clear" - assert state.name == "Weather Summary" - assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - assert ( - state.attributes["entity_picture"] == "https://icons.wxug.com/i/c/k/clear.gif" - ) - - state = hass.states.get("sensor.pws_alerts") - assert state.state == "1" - assert state.name == "Alerts" - assert state.attributes["Message"] == "This is a test alert message" - assert state.attributes["icon"] == "mdi:alert-circle-outline" - assert "entity_picture" not in state.attributes - - state = hass.states.get("sensor.pws_location") - assert state.state == "Holly Springs, NC" - assert state.name == "Location" - - state = hass.states.get("sensor.pws_elevation") - assert state.state == "413" - assert state.name == "Elevation" - - state = hass.states.get("sensor.pws_feelslike_c") - assert state.state == "40" - assert state.name == "Feels Like" - assert "entity_picture" not in state.attributes - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - - state = hass.states.get("sensor.pws_weather_1d_metric") - assert state.state == "Mostly Cloudy. Fog overnight." - assert state.name == "Tuesday" - - state = hass.states.get("sensor.pws_precip_1d_in") - assert state.state == "0.03" - assert state.name == "Precipitation Intensity Today" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_INCHES - - -async def test_connect_failed(hass, aioclient_mock): - """Test the WUnderground connection error.""" - aioclient_mock.get(URL, exc=aiohttp.ClientError()) - with raises(PlatformNotReady): - await wunderground.async_setup_platform(hass, VALID_CONFIG, lambda _: None) - - -async def test_invalid_data(hass, aioclient_mock): - """Test the WUnderground invalid data.""" - aioclient_mock.get(URL, text=load_fixture("wunderground-invalid.json")) - - await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) - await hass.async_block_till_done() - - for condition in VALID_CONFIG["monitored_conditions"]: - state = hass.states.get(f"sensor.pws_{condition}") - assert state.state == STATE_UNKNOWN - - -async def test_entity_id_with_multiple_stations(hass, aioclient_mock): - """Test not generating duplicate entity ids with multiple stations.""" - aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) - aioclient_mock.get(PWS_URL, text=load_fixture("wunderground-valid.json")) - - config = [VALID_CONFIG, {**VALID_CONFIG_PWS, "entity_namespace": "hi"}] - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() - - state = hass.states.get("sensor.pws_weather") - assert state is not None - assert state.state == "Clear" - - state = hass.states.get("sensor.hi_pws_weather") - assert state is not None - assert state.state == "Clear" - - -async def test_fails_because_of_unique_id(hass, aioclient_mock): - """Test same config twice fails because of unique_id.""" - aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) - aioclient_mock.get(PWS_URL, text=load_fixture("wunderground-valid.json")) - - config = [ - VALID_CONFIG, - {**VALID_CONFIG, "entity_namespace": "hi"}, - VALID_CONFIG_PWS, - ] - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() - - states = hass.states.async_all() - expected = len(VALID_CONFIG["monitored_conditions"]) + len( - VALID_CONFIG_PWS["monitored_conditions"] - ) - assert len(states) == expected diff --git a/tests/fixtures/wunderground-error.json b/tests/fixtures/wunderground-error.json deleted file mode 100644 index 264ecbf8cd6..00000000000 --- a/tests/fixtures/wunderground-error.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "response": { - "version": "0.1", - "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", - "features": {}, - "error": { - "type": "keynotfound", - "description": "this key does not exist" - } - } -} diff --git a/tests/fixtures/wunderground-invalid.json b/tests/fixtures/wunderground-invalid.json deleted file mode 100644 index 59661c6694d..00000000000 --- a/tests/fixtures/wunderground-invalid.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "response": { - "version": "0.1", - "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", - "features": { - "conditions": 1, - "alerts": 1, - "forecast": 1 - } - }, - "current_observation": { - "image": { - "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png", - "title": "Weather Underground", - "link": "http://www.wunderground.com" - } - } -} diff --git a/tests/fixtures/wunderground-valid.json b/tests/fixtures/wunderground-valid.json deleted file mode 100644 index 7ac1081cb4e..00000000000 --- a/tests/fixtures/wunderground-valid.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "response": { - "version": "0.1", - "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", - "features": { - "conditions": 1, - "alerts": 1, - "forecast": 1 - } - }, - "current_observation": { - "image": { - "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png", - "title": "Weather Underground", - "link": "http://www.wunderground.com" - }, - "feelslike_c": "40", - "weather": "Clear", - "icon_url": "http://icons.wxug.com/i/c/k/clear.gif", - "display_location": { - "city": "Holly Springs", - "country": "US", - "full": "Holly Springs, NC" - }, - "observation_location": { - "elevation": "413 ft", - "full": "Twin Lake, Holly Springs, North Carolina" - } - }, - "alerts": [ - { - "type": "FLO", - "description": "Areal Flood Warning", - "date": "9:36 PM CDT on September 22, 2016", - "expires": "10:00 AM CDT on September 23, 2016", - "message": "This is a test alert message" - } - ], - "forecast": { - "txt_forecast": { - "date": "22:35 CEST", - "forecastday": [ - { - "period": 0, - "icon_url": "http://icons.wxug.com/i/c/k/clear.gif", - "title": "Tuesday", - "fcttext": "Mostly Cloudy. Fog overnight.", - "fcttext_metric": "Mostly Cloudy. Fog overnight.", - "pop": "0" - } - ] - }, - "simpleforecast": { - "forecastday": [ - { - "date": { - "pretty": "19:00 CEST 4. Duben 2017" - }, - "period": 1, - "high": { - "fahrenheit": "56", - "celsius": "13" - }, - "low": { - "fahrenheit": "43", - "celsius": "6" - }, - "conditions": "Mo\u017enost de\u0161t\u011b", - "icon_url": "http://icons.wxug.com/i/c/k/chancerain.gif", - "qpf_allday": { - "in": 0.03, - "mm": 1 - }, - "maxwind": { - "mph": 0, - "kph": 0, - "dir": "", - "degrees": 0 - }, - "avewind": { - "mph": 0, - "kph": 0, - "dir": "severn\u00ed", - "degrees": 0 - } - } - ] - } - } -} From c360d6009c1f6b33c86e5f992bd690b81163439c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 14 Jul 2021 11:00:16 +0200 Subject: [PATCH 282/818] copy() --> deepcopy(). (#52794) --- homeassistant/components/modbus/modbus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0826f4d5794..f2b033bec3e 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -1,5 +1,6 @@ """Support for Modbus.""" import asyncio +from copy import deepcopy import logging from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient @@ -196,7 +197,7 @@ class ModbusHub: self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_delay = client_config[CONF_DELAY] - self._pb_call = PYMODBUS_CALL.copy() + self._pb_call = deepcopy(PYMODBUS_CALL) self._pb_class = { CONF_SERIAL: ModbusSerialClient, CONF_TCP: ModbusTcpClient, From 30d465e9dd461700af6426018ae42cde904b659c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Jul 2021 23:44:58 -1000 Subject: [PATCH 283/818] Update homekit to use network integration (#52946) --- homeassistant/components/homekit/__init__.py | 12 ++++---- .../components/homekit/manifest.json | 2 +- .../components/homekit/type_cameras.py | 3 +- tests/components/homekit/test_homekit.py | 29 +++++++++++-------- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c0cc5867799..842bcf81efe 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,7 +8,7 @@ from aiohttp import web from pyhap.const import STANDALONE_AID import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import network, zeroconf from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_MOTION, @@ -40,7 +40,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.loader import IntegrationNotFound, async_get_integration -from homeassistant.util import get_local_ip from . import ( # noqa: F401 type_cameras, @@ -119,6 +118,8 @@ STATUS_WAIT = 3 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 +MDNS_TARGET_IP = "224.0.0.251" + def _has_all_unique_names_and_ports(bridges): """Validate that each homekit bridge configured has a unique name.""" @@ -243,7 +244,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Begin setup HomeKit for %s", name) # ip_address and advertise_ip are yaml only - ip_address = conf.get(CONF_IP_ADDRESS) + ip_address = conf.get( + CONF_IP_ADDRESS, await network.async_get_source_ip(hass, MDNS_TARGET_IP) + ) advertise_ip = conf.get(CONF_ADVERTISE_IP) # exclude_accessory_mode is only used for config flow # to indicate that the config entry was setup after @@ -458,7 +461,6 @@ class HomeKit: def setup(self, async_zeroconf_instance): """Set up bridge and accessory driver.""" - ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) self.driver = HomeDriver( @@ -467,7 +469,7 @@ class HomeKit: self._name, self._entry_title, loop=self.hass.loop, - address=ip_addr, + address=self._ip_address, port=self._port, persist_file=persist_file, advertised_address=self._advertise_ip, diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 39c40e03614..40bca03bae0 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,7 +9,7 @@ "base36==0.1.1", "PyTurboJPEG==1.5.0" ], - "dependencies": ["http", "camera", "ffmpeg"], + "dependencies": ["http", "camera", "ffmpeg", "network"], "after_dependencies": ["zeroconf"], "codeowners": ["@bdraco"], "zeroconf": ["_homekit._tcp.local."], diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 48f7ad9b064..e21ca189b4e 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -18,7 +18,6 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, ) -from homeassistant.util import get_local_ip from .accessories import TYPES, HomeAccessory from .const import ( @@ -181,7 +180,7 @@ class Camera(HomeAccessory, PyhapCamera): ] } - stream_address = config.get(CONF_STREAM_ADDRESS, get_local_ip()) + stream_address = config.get(CONF_STREAM_ADDRESS, driver.state.address) options = { "video": video_options, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 5e9ea4fd4b6..7fd588e62d0 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -156,7 +156,9 @@ async def test_setup_min(hass, mock_zeroconf): ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) @@ -166,7 +168,7 @@ async def test_setup_min(hass, mock_zeroconf): hass, BRIDGE_NAME, DEFAULT_PORT, - None, + "1.2.3.4", ANY, ANY, {}, @@ -249,7 +251,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): hass, BRIDGE_NAME, DEFAULT_PORT, - None, + IP_ADDRESS, True, {}, {}, @@ -262,10 +264,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): hass.states.async_set("light.demo", "on") hass.states.async_set("light.demo2", "on") zeroconf_mock = MagicMock() - with patch( - f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver - ) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip: - mock_ip.return_value = IP_ADDRESS + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_executor_job(homekit.setup, zeroconf_mock) path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) @@ -842,7 +841,9 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await async_setup_component( @@ -854,7 +855,7 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): hass, BRIDGE_NAME, 12345, - None, + "1.2.3.4", ANY, ANY, {}, @@ -1109,7 +1110,9 @@ async def test_reload(hass, mock_zeroconf): ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): mock_homekit.return_value = homekit = Mock() assert await async_setup_component( hass, "homekit", {"homekit": {CONF_NAME: "reloadable", CONF_PORT: 12345}} @@ -1120,7 +1123,7 @@ async def test_reload(hass, mock_zeroconf): hass, "reloadable", 12345, - None, + "1.2.3.4", ANY, False, {}, @@ -1142,6 +1145,8 @@ async def test_reload(hass, mock_zeroconf): f"{PATH_HOMEKIT}.get_accessory" ), patch( "pyhap.accessory_driver.AccessoryDriver.async_start" + ), patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" ): mock_homekit2.return_value = homekit = Mock() await hass.services.async_call( @@ -1156,7 +1161,7 @@ async def test_reload(hass, mock_zeroconf): hass, "reloadable", 45678, - None, + "1.2.3.4", ANY, False, {}, From ddc788bf8e241b5bffcef6d679b12b2bbe9f80fe Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Jul 2021 11:54:55 +0200 Subject: [PATCH 284/818] Include future statistics in history/list_statistic_ids (#52942) * Include future statistics in history/list_statistic_ids * Improve tests --- .../components/recorder/statistics.py | 22 +++++- homeassistant/components/sensor/recorder.py | 35 +++++++- tests/components/history/test_init.py | 31 +++++--- tests/components/sensor/test_recorder.py | 79 +++++++++++++++++-- 4 files changed, 149 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2583b2f53b6..b6af3d60d73 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -196,6 +196,7 @@ def list_statistic_ids( ) -> list[dict[str, str] | None]: """Return statistic_ids and meta data.""" units = hass.config.units + statistic_ids = {} with session_scope(hass=hass) as session: metadata = _get_metadata(hass, session, None, statistic_type) @@ -203,7 +204,26 @@ def list_statistic_ids( unit = _configured_unit(meta["unit_of_measurement"], units) meta["unit_of_measurement"] = unit - return list(metadata.values()) + statistic_ids = { + meta["statistic_id"]: meta["unit_of_measurement"] + for meta in metadata.values() + } + + for platform in hass.data[DOMAIN].values(): + if not hasattr(platform, "list_statistic_ids"): + continue + platform_statistic_ids = platform.list_statistic_ids(hass, statistic_type) + + for statistic_id, unit in platform_statistic_ids.items(): + unit = _configured_unit(unit, units) + platform_statistic_ids[statistic_id] = unit + + statistic_ids = {**statistic_ids, **platform_statistic_ids} + + return [ + {"statistic_id": _id, "unit_of_measurement": unit} + for _id, unit in statistic_ids.items() + ] def statistics_during_period( diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 485674ec728..b5a38cfeec1 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -40,7 +40,7 @@ import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util -from . import DOMAIN +from . import ATTR_LAST_RESET, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -280,3 +280,36 @@ def compile_statistics( result[entity_id]["stat"] = stat return result + + +def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) -> dict: + """Return statistic_ids and meta data.""" + entities = _get_entities(hass) + + statistic_ids = {} + + for entity_id, device_class in entities: + provided_statistics = DEVICE_CLASS_STATISTICS[device_class] + + if statistic_type is not None and statistic_type not in provided_statistics: + continue + + state = hass.states.get(entity_id) + assert state + + if "sum" in provided_statistics and ATTR_LAST_RESET not in state.attributes: + continue + + native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if device_class not in UNIT_CONVERSIONS: + statistic_ids[entity_id] = native_unit + continue + + if native_unit not in UNIT_CONVERSIONS[device_class]: + continue + + statistics_unit = DEVICE_CLASS_UNITS[device_class] + statistic_ids[entity_id] = statistics_unit + + return statistic_ids diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index df4a59a372c..8d78f80c634 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -988,11 +988,6 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) await async_setup_component(hass, "history", {"history": {}}) await async_setup_component(hass, "sensor", {}) await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) - hass.states.async_set("sensor.test", 10, attributes=attributes) - await hass.async_block_till_done() - - await hass.async_add_executor_job(trigger_db_commit, hass) - await hass.async_block_till_done() client = await hass_ws_client() await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) @@ -1000,8 +995,11 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["success"] assert response["result"] == [] - hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) - await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) response = await client.receive_json() @@ -1010,15 +1008,28 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) {"statistic_id": "sensor.test", "unit_of_measurement": unit} ] + hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + # Remove the state, statistics will now be fetched from the database + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "history/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + {"statistic_id": "sensor.test", "unit_of_measurement": unit} + ] + await client.send_json( - {"id": 3, "type": "history/list_statistic_ids", "statistic_type": "dogs"} + {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "dogs"} ) response = await client.receive_json() assert response["success"] assert response["result"] == [] await client.send_json( - {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "mean"} + {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "mean"} ) response = await client.receive_json() assert response["success"] @@ -1027,7 +1038,7 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) ] await client.send_json( - {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "sum"} + {"id": 6, "type": "history/list_statistic_ids", "statistic_type": "sum"} ) response = await client.receive_json() assert response["success"] diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 99ede396381..998cc93e629 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -120,12 +120,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes attributes.pop("state_class") _, _states = record_states(hass, zero, "sensor.test5", attributes) states = {**states, **_states} - attributes["state_class"] = "measurement" - _, _states = record_states(hass, zero, "sensor.test6", attributes) - states = {**states, **_states} - attributes["state_class"] = "unsupported" - _, _states = record_states(hass, zero, "sensor.test7", attributes) - states = {**states, **_states} hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) @@ -626,6 +620,79 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): assert "Error while processing event StatisticsTask" in caplog.text +@pytest.mark.parametrize( + "device_class,unit,native_unit,statistic_type", + [ + ("battery", "%", "%", "mean"), + ("battery", None, None, "mean"), + ("energy", "Wh", "kWh", "sum"), + ("energy", "kWh", "kWh", "sum"), + ("humidity", "%", "%", "mean"), + ("humidity", None, None, "mean"), + ("monetary", "USD", "USD", "sum"), + ("monetary", "None", "None", "sum"), + ("pressure", "Pa", "Pa", "mean"), + ("pressure", "hPa", "Pa", "mean"), + ("pressure", "mbar", "Pa", "mean"), + ("pressure", "inHg", "Pa", "mean"), + ("pressure", "psi", "Pa", "mean"), + ("temperature", "°C", "°C", "mean"), + ("temperature", "°F", "°C", "mean"), + ], +) +def test_list_statistic_ids( + hass_recorder, caplog, device_class, unit, native_unit, statistic_type +): + """Test listing future statistic ids.""" + hass = hass_recorder() + setup_component(hass, "sensor", {}) + attributes = { + "device_class": device_class, + "last_reset": 0, + "state_class": "measurement", + "unit_of_measurement": unit, + } + hass.states.set("sensor.test1", 0, attributes=attributes) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + for stat_type in ["mean", "sum", "dogs"]: + statistic_ids = list_statistic_ids(hass, statistic_type=stat_type) + if statistic_type == stat_type: + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + else: + assert statistic_ids == [] + + +@pytest.mark.parametrize( + "_attributes", + [{**ENERGY_SENSOR_ATTRIBUTES, "last_reset": 0}, TEMPERATURE_SENSOR_ATTRIBUTES], +) +def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): + """Test listing future statistic ids for unsupported sensor.""" + hass = hass_recorder() + setup_component(hass, "sensor", {}) + attributes = dict(_attributes) + hass.states.set("sensor.test1", 0, attributes=attributes) + if "last_reset" in attributes: + attributes.pop("unit_of_measurement") + hass.states.set("last_reset.test2", 0, attributes=attributes) + attributes = dict(_attributes) + if "unit_of_measurement" in attributes: + attributes["unit_of_measurement"] = "invalid" + hass.states.set("sensor.test3", 0, attributes=attributes) + attributes.pop("unit_of_measurement") + hass.states.set("sensor.test4", 0, attributes=attributes) + attributes = dict(_attributes) + attributes["state_class"] = "invalid" + hass.states.set("sensor.test5", 0, attributes=attributes) + attributes.pop("state_class") + hass.states.set("sensor.test6", 0, attributes=attributes) + + def record_states(hass, zero, entity_id, attributes): """Record some test states. From 8c36f5c62721e04dc1d3e9ba07a16738d2cfa8ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Jul 2021 12:07:00 +0200 Subject: [PATCH 285/818] Deprecate Lyft integration (#53005) --- homeassistant/components/lyft/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index c979231a216..39cfff38a1b 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -39,6 +39,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Lyft sensor.""" + _LOGGER.warning( + "The Lyft integration has been deprecated and will be removed in " + "Home Assistant Core 2021.10" + ) auth_flow = ClientCredentialGrant( client_id=config.get(CONF_CLIENT_ID), From e541bcd54d050bd5acf63137786cc1ec70fa6520 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Jul 2021 13:23:11 +0200 Subject: [PATCH 286/818] Update statistics meta data on entity_id change (#52755) --- .../components/recorder/statistics.py | 28 ++++++++- tests/components/recorder/test_statistics.py | 58 +++++++++++++++++++ tests/conftest.py | 2 +- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index b6af3d60d73..f3b0b27df39 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -12,7 +12,8 @@ from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util @@ -73,6 +74,31 @@ def async_setup(hass: HomeAssistant) -> None: hass.data[STATISTICS_BAKERY] = baked.bakery() hass.data[STATISTICS_META_BAKERY] = baked.bakery() + def entity_id_changed(event: Event) -> None: + """Handle entity_id changed.""" + old_entity_id = event.data["old_entity_id"] + entity_id = event.data["entity_id"] + with session_scope(hass=hass) as session: + session.query(StatisticsMeta).filter( + StatisticsMeta.statistic_id == old_entity_id + and StatisticsMeta.source == DOMAIN + ).update({StatisticsMeta.statistic_id: entity_id}) + + @callback + def entity_registry_changed_filter(event: Event) -> bool: + """Handle entity_id changed filter.""" + if event.data["action"] != "update" or "old_entity_id" not in event.data: + return False + + return True + + if hass.is_running: + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + entity_id_changed, + event_filter=entity_registry_changed_filter, + ) + def get_start_time() -> datetime: """Return start time.""" diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 32eaaaab842..0468cc26a23 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -16,6 +16,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.common import mock_registry from tests.components.recorder.common import wait_recording_done @@ -93,6 +94,63 @@ def test_compile_hourly_statistics(hass_recorder): assert stats == {} +def test_rename_entity(hass_recorder): + """Test statistics is migrated when entity_id is changed.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + + entity_reg = mock_registry(hass) + reg_entry = entity_reg.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): + stats = statistics_during_period(hass, zero, **kwargs) + assert stats == {} + stats = get_last_statistics(hass, 0, "sensor.test1") + assert stats == {} + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + expected_1 = { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_stats1 = [ + {**expected_1, "statistic_id": "sensor.test1"}, + ] + expected_stats2 = [ + {**expected_1, "statistic_id": "sensor.test2"}, + ] + expected_stats99 = [ + {**expected_1, "statistic_id": "sensor.test99"}, + ] + + stats = statistics_during_period(hass, zero) + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + entity_reg.async_update_entity(reg_entry.entity_id, new_entity_id="sensor.test99") + hass.block_till_done() + + stats = statistics_during_period(hass, zero) + assert stats == {"sensor.test99": expected_stats99, "sensor.test2": expected_stats2} + + def record_states(hass): """Record some test states. diff --git a/tests/conftest.py b/tests/conftest.py index 1f5ffc80d0d..ce8e244f420 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -610,7 +610,7 @@ def enable_statistics(): @pytest.fixture -def hass_recorder(enable_statistics): +def hass_recorder(enable_statistics, hass_storage): """Home Assistant fixture with in-memory recorder.""" hass = get_test_home_assistant() stats = recorder.Recorder.async_hourly_statistics if enable_statistics else None From 1f15181522e881d301687f027382d46a1d9637c8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 14 Jul 2021 10:14:13 -0400 Subject: [PATCH 287/818] Add support for Z-Wave JS siren (#52948) * Add support for Z-Wave JS siren * Add additional device class to discovery * fix docstring * Remove device class specific part of discovery schema * rename test * switch to entry.async_on_remove * Fix logic based on #52971 * Use constants to unblock PR * Add support to set volume level * Update homeassistant/components/zwave_js/siren.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/const.py | 4 + .../components/zwave_js/discovery.py | 9 + homeassistant/components/zwave_js/siren.py | 105 + tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_siren.py | 145 + .../zwave_js/aeotec_zw164_siren_state.json | 3748 +++++++++++++++++ 6 files changed, 4025 insertions(+) create mode 100644 homeassistant/components/zwave_js/siren.py create mode 100644 tests/components/zwave_js/test_siren.py create mode 100644 tests/fixtures/zwave_js/aeotec_zw164_siren_state.json diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 2bbe35de664..d1b9ecaaa15 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -69,3 +69,7 @@ ATTR_BROADCAST = "broadcast" SERVICE_PING = "ping" ADDON_SLUG = "core_zwave_js" + +# Siren constants +TONE_ID_DEFAULT = 255 +TONE_ID_OFF = 0 diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 29976850480..403ea5c9746 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -175,6 +175,10 @@ SWITCH_BINARY_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"} ) +SIREN_TONE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.SOUND_SWITCH}, property={"toneId"}, type={"number"} +) + # For device class mapping see: # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ @@ -582,6 +586,11 @@ DISCOVERY_SCHEMAS = [ platform="light", primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # sirens + ZWaveDiscoverySchema( + platform="siren", + primary_value=SIREN_TONE_SCHEMA, + ), ] diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py new file mode 100644 index 00000000000..fa6e24878ed --- /dev/null +++ b/homeassistant/components/zwave_js/siren.py @@ -0,0 +1,105 @@ +"""Support for Z-Wave controls using the siren platform.""" +from __future__ import annotations + +from typing import Any + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN, SirenEntity +from homeassistant.components.siren.const import ( + ATTR_TONE, + ATTR_VOLUME_LEVEL, + SUPPORT_TONES, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_CLIENT, DOMAIN, TONE_ID_DEFAULT, TONE_ID_OFF +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Z-Wave Siren entity from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_siren(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave siren entity.""" + entities: list[ZWaveBaseEntity] = [] + entities.append(ZwaveSirenEntity(config_entry, client, info)) + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{SIREN_DOMAIN}", + async_add_siren, + ) + ) + + +class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): + """Representation of a Z-Wave siren entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveSirenEntity entity.""" + super().__init__(config_entry, client, info) + # Entity class attributes + self._attr_available_tones = list( + self.info.primary_value.metadata.states.values() + ) + self._attr_supported_features = ( + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET + ) + if self._attr_available_tones: + self._attr_supported_features |= SUPPORT_TONES + + @property + def is_on(self) -> bool: + """Return whether device is on.""" + return bool(self.info.primary_value.value) + + async def async_set_value( + self, new_value: int, options: dict[str, Any] | None = None + ) -> None: + """Set a value on a siren node.""" + await self.info.node.async_set_value( + self.info.primary_value, new_value, options=options + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + tone: str | None = kwargs.get(ATTR_TONE) + options = {} + if (volume := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: + options["volume"] = round(volume * 100) + # Play the default tone if a tone isn't provided + if tone is None: + await self.async_set_value(TONE_ID_DEFAULT, options) + return + + tone_id = int( + next( + key + for key, value in self.info.primary_value.metadata.states.items() + if value == tone + ) + ) + + await self.async_set_value(tone_id, options) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self.async_set_value(TONE_ID_OFF) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index f0c69709031..f62c7fb8b9f 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -429,6 +429,12 @@ def wallmote_central_scene_state_fixture(): return json.loads(load_fixture("zwave_js/wallmote_central_scene_state.json")) +@pytest.fixture(name="aeotec_zw164_siren_state", scope="session") +def aeotec_zw164_siren_state_fixture(): + """Load the aeotec zw164 siren node state fixture data.""" + return json.loads(load_fixture("zwave_js/aeotec_zw164_siren_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -789,6 +795,14 @@ def wallmote_central_scene_fixture(client, wallmote_central_scene_state): return node +@pytest.fixture(name="aeotec_zw164_siren") +def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): + """Mock a wallmote central scene node.""" + node = Node(client, copy.deepcopy(aeotec_zw164_siren_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="firmware_file") def firmware_file_fixture(): """Return mock firmware file stream.""" diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py new file mode 100644 index 00000000000..23507e6a705 --- /dev/null +++ b/tests/components/zwave_js/test_siren.py @@ -0,0 +1,145 @@ +"""Test the Z-Wave JS siren platform.""" +from zwave_js_server.event import Event + +from homeassistant.components.siren import ATTR_TONE, ATTR_VOLUME_LEVEL +from homeassistant.const import STATE_OFF, STATE_ON + +SIREN_ENTITY = "siren.indoor_siren_6_2" + +TONE_ID_VALUE_ID = { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default", + }, + }, +} + + +async def test_siren(hass, client, aeotec_zw164_siren, integration): + """Test the siren entity.""" + node = aeotec_zw164_siren + state = hass.states.get(SIREN_ENTITY) + + assert state + assert state.state == STATE_OFF + + # Test turn on with default + await hass.services.async_call( + "siren", + "turn_on", + {"entity_id": SIREN_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == TONE_ID_VALUE_ID + assert args["value"] == 255 + + client.async_send_command.reset_mock() + + # Test turn on with specific tone name and volume level + await hass.services.async_call( + "siren", + "turn_on", + { + "entity_id": SIREN_ENTITY, + ATTR_TONE: "01DING~1 (5 sec)", + ATTR_VOLUME_LEVEL: 0.5, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == TONE_ID_VALUE_ID + assert args["value"] == 1 + assert args["options"] == {"volume": 50} + + client.async_send_command.reset_mock() + + # Test turn off + await hass.services.async_call( + "siren", + "turn_off", + {"entity_id": SIREN_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == TONE_ID_VALUE_ID + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "toneId", + "newValue": 255, + "prevValue": 0, + "propertyName": "toneId", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(SIREN_ENTITY) + assert state.state == STATE_ON diff --git a/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json b/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json new file mode 100644 index 00000000000..6bf7ece9758 --- /dev/null +++ b/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json @@ -0,0 +1,3748 @@ +{ + "nodeId": 2, + "index": 0, + "installerIcon": 8704, + "userIcon": 8704, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 881, + "productId": 164, + "productType": 259, + "firmwareVersion": "1.3", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0371/zw164.json", + "manufacturer": "Aeotec Ltd.", + "manufacturerId": 881, + "label": "ZW164", + "description": "Indoor Siren 6", + "devices": [ + { + "productType": 3, + "productId": 164 + }, + { + "productType": 259, + "productId": 164 + }, + { + "productType": 515, + "productId": 164 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "This product supports Security 2 Command Class. While a Security S2 enabled Controller is needed in order to fully use the security feature. This product can be included and operated in any Z-Wave network with other Z-Wave certified devices from other manufacturers and/or other applications. All non-battery operated nodes within the network will\nact as repeaters regardless of vendor to increase reliability of the network.\n\n1. Set your Z-Wave Controller into its 'Add Device' mode in order to add Chime into your Z-Wave system. Refer to the Controller's manual if you are unsure of how to perform this step.\n\n2. Power on Chime via the provided power adapter; its LED will be breathing white light all the time.\n\n3. Click Chime Action Button once, it will quickly flash white light for 30 seconds until Chime is added into the network. It will become constantly bright white light after being assigned a NodeID.\n\n4. If your Z-Wave Controller supports S2 encryption, enter the first 5 digits of DSK into your Controller's interface if/when requested. The DSK is printed on Chime's housing.\n\n5. If Adding fails, it will slowly flash white light 3 times and then become breathing white light; repeat steps 1 to 4. Contact us for further support if needed.\n\n6. If Adding succeeds, it will quickly flash white light 3 times and then become off. Now, Chime is a part of your Z-Wave home control system. You can configure it and its automations via your Z-Wave system; please refer to your software's user guide for precise instructions.\n\nNote:\nIf Action Button is clicked again during the Learn Mode, the Learn Mode will exit. At the same time, Indicator Light will extinguish immediately, and then become breathing white light", + "exclusion": "1. Set your Z-Wave Controller into its ' Remove Device' mode in order to remove Chime from your Z-Wave system. Refer to the Controller's manual if you are unsure of how to perform this step.\n\n2. Power on Chime via the provided power adapter; its LED will be off.\n\n3. Click Chime Action Button 6 times quickly; it will bright white light, up to 2s.\n\n4. If Removing fails, it will keep off; repeat steps 1 to 3. Contact us for further support if needed.\n\n5. If Removing succeeds, it will quickly flash white light 3 times and then become breathing white light. Now, Chime is removed from Z-Wave network successfully", + "reset": "If the primary controller is missing or inoperable, you may need to reset the device to factory settings.\n\nMake sure the Chime is powered. To complete the reset process manually, press and hold the Action Button for at least 20s. The LED indicator will quickly flash white light 3 times and then become breathing white light, which indicates the reset operation is successful. Otherwise, please try again. Contact us for further support if needed.\n\nNote:\n1. This procedure should only be used when the primary controller is missing or inoperable.\n2. Factory Reset Chime will:\n(a) Remove Chime from Z-Wave network;\n(b) Delete the Association setting;\n(c) Restore the configuration settings to the default.(Except configuration parameter 51/52/53/54)", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/3301/Indoor%20Siren%206%20product%20manual.pdf" + }, + "isEmbedded": true + }, + "label": "ZW164", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": true, + "individualEndpointCount": 8, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 1, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 2, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 3, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 4, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 5, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 6, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 7, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 2, + "index": 8, + "installerIcon": 8704, + "userIcon": 8704, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 881 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 259 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 164 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "5.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.3"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 164 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyName": "Group 2 Basic Set Command (Browse)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 2 Basic Set Command (Browse)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyName": "Group 3 Basic Set Command (Tampering)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 3 Basic Set Command (Tampering)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyName": "Group 4 Basic Set Command (Doorbell 1)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 4 Basic Set Command (Doorbell 1)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 35, + "propertyName": "Group 5 Basic Set Command (Doorbell 2)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 5 Basic Set Command (Doorbell 2)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 36, + "propertyName": "Group 6 Basic Set Command (Doorbell 3)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 6 Basic Set Command (Doorbell 3)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyName": "Group 7 Basic Set Command (Environment)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 7 Basic Set Command (Environment)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyName": "Group 8 Basic Set Command (Security)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 8 Basic Set Command (Security)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 39, + "propertyName": "Group 9 Basic Set Command (Emergency)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Group 9 Basic Set Command (Emergency)", + "default": 3, + "min": 0, + "max": 4, + "states": { + "0": "Disable", + "1": "Start playing -> On; Stop playing -> None", + "2": "Start playing -> Off; Stop playing -> None", + "3": "Start playing -> On; Stop playing -> Off", + "4": "Start playing -> Off; Stop playing -> On" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyName": "Pairing Mode Status", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Pairing Mode Status", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "Not pairing", + "1": "Pairing Button No. 1", + "2": "Pairing Button No. 1", + "4": "Pairing Button No. 1" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Browse)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Browse)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyKey": 16711680, + "propertyName": "Tone Play Mode (Browse)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Mode (Browse)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Single playback", + "1": "Single loop playback", + "2": "Loop playback tones", + "3": "Random playback tones", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Tamper)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Tamper)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Tamper)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Tamper)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Tamper)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Tamper)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyKey": 255, + "propertyName": "Tone Play Count (Tamper)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Tamper)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Doorbell 1)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Doorbell 1)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Doorbell 1)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Doorbell 1)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Doorbell 1)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Doorbell 1)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 255, + "propertyName": "Tone Play Count (Doorbell 1)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Doorbell 1)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Doorbell 2)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Doorbell 2)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Doorbell 2)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Doorbell 2)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Doorbell 2)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Doorbell 2)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyKey": 255, + "propertyName": "Tone Play Count (Doorbell 2)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Doorbell 2)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Doorbell 3)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Doorbell 3)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Doorbell 3)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Doorbell 3)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Doorbell 3)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Doorbell 3)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyKey": 255, + "propertyName": "Tone Play Count (Doorbell 3)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Doorbell 3)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Environment)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Environment)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Environment)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Environment)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Environment)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Environment)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyKey": 255, + "propertyName": "Tone Play Count (Environment)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Environment)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Security)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Security)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Security)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Security)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Security)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Security)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyKey": 255, + "propertyName": "Tone Play Count (Security)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Security)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyKey": 4278190080, + "propertyName": "Light Effect Index (Emergency)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect Index (Emergency)", + "default": 1, + "min": 1, + "max": 127, + "states": { + "1": "Light effect #1", + "2": "Light effect #2", + "4": "Light effect #3", + "8": "Light effect #4", + "16": "Light effect #5", + "32": "Light effect #6", + "64": "Light effect #7", + "127": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyKey": 16711680, + "propertyName": "Tone Duration (Emergency)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Duration (Emergency)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Original tone length", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyKey": 65280, + "propertyName": "Interval Between Tones (Emergency)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval Between Tones (Emergency)", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "No interval", + "255": "Last configuration value" + }, + "unit": "seconds", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyKey": 255, + "propertyName": "Tone Play Count (Emergency)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone Play Count (Emergency)", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Unlimited", + "255": "Last configuration value" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 1: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 1: Dim On Duration", + "default": 75, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 75 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 1: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 1: Dim Off Duration", + "default": 25, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 25 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyKey": 65280, + "propertyName": "Light Effect No. 1: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 1: LED Indicator On Duration", + "default": 20, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyKey": 255, + "propertyName": "Light Effect No. 1: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 1: LED Indicator Off Duration", + "default": 3, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 2: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 2: Dim On Duration", + "default": 50, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 2: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 2: Dim Off Duration", + "default": 50, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyKey": 65280, + "propertyName": "Light Effect No. 2: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 2: LED Indicator On Duration", + "default": 0, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyKey": 255, + "propertyName": "Light Effect No. 2: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 2: LED Indicator Off Duration", + "default": 3, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 3: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 3: Dim On Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 3: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 3: Dim Off Duration", + "default": 33, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 33 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyKey": 65280, + "propertyName": "Light Effect No. 3: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 3: LED Indicator On Duration", + "default": 1, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyKey": 255, + "propertyName": "Light Effect No. 3: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 3: LED Indicator Off Duration", + "default": 3, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 4: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 4: Dim On Duration", + "default": 33, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 33 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 4: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 4: Dim Off Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyKey": 65280, + "propertyName": "Light Effect No. 4: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 4: LED Indicator On Duration", + "default": 0, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyKey": 255, + "propertyName": "Light Effect No. 4: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 4: LED Indicator Off Duration", + "default": 3, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 5: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 5: Dim On Duration", + "default": 33, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 5: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 5: Dim Off Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyKey": 65280, + "propertyName": "Light Effect No. 5: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 5: LED Indicator On Duration", + "default": 0, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyKey": 255, + "propertyName": "Light Effect No. 5: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 5: LED Indicator Off Duration", + "default": 3, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 6: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 6: Dim On Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 6: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 6: Dim Off Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyKey": 65280, + "propertyName": "Light Effect No. 6: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 6: LED Indicator On Duration", + "default": 10, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyKey": 255, + "propertyName": "Light Effect No. 6: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 6: LED Indicator Off Duration", + "default": 0, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyKey": 4278190080, + "propertyName": "Light Effect No. 7: Dim On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 7: Dim On Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 33 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyKey": 16711680, + "propertyName": "Light Effect No. 7: Dim Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 7: Dim Off Duration", + "default": 0, + "min": 0, + "max": 127, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyKey": 65280, + "propertyName": "Light Effect No. 7: LED Indicator On Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 7: LED Indicator On Duration", + "default": 0, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyKey": 255, + "propertyName": "Light Effect No. 7: LED Indicator Off Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Effect No. 7: LED Indicator Off Duration", + "default": 1, + "min": 0, + "max": 255, + "unit": "ms", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 51, + "propertyKey": 1, + "propertyName": "Status: Button 1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Status: Button 1", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Not paired", + "1": "Paired" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 51, + "propertyKey": 2, + "propertyName": "Status: Button 2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Status: Button 2", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Not paired", + "1": "Paired" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 51, + "propertyKey": 4, + "propertyName": "Status: Button 3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Status: Button 3", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Not paired", + "1": "Paired" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 52, + "propertyKey": 4294901760, + "propertyName": "Button 1: Battery Voltage", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Button 1: Battery Voltage", + "default": 0, + "min": 0, + "max": 32767, + "states": { + "0": "Not paired" + }, + "unit": "mV", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 52, + "propertyKey": 65535, + "propertyName": "Button 1: Software Version", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button 1: Software Version", + "default": 0, + "min": 0, + "max": 65535, + "states": { + "0": "Not paired" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 53, + "propertyKey": 4294901760, + "propertyName": "Button 2: Battery Voltage", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Button 2: Battery Voltage", + "default": 0, + "min": 0, + "max": 32767, + "states": { + "0": "Not paired" + }, + "unit": "mV", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 53, + "propertyKey": 65535, + "propertyName": "Button 2: Software Version", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button 2: Software Version", + "default": 0, + "min": 0, + "max": 65535, + "states": { + "0": "Not paired" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 54, + "propertyKey": 4294901760, + "propertyName": "Button 3: Battery Voltage", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Button 3: Battery Voltage", + "default": 0, + "min": 0, + "max": 32767, + "states": { + "0": "Not paired" + }, + "unit": "mV", + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 54, + "propertyKey": 65535, + "propertyName": "Button 3: Software Version", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button 3: Software Version", + "default": 0, + "min": 0, + "max": 65535, + "states": { + "0": "Not paired" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 48, + "propertyName": "Button Unpairing", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Button Unpairing", + "default": 0, + "min": 0, + "max": 7, + "states": { + "0": "Normal Operation", + "1": "Unpair Button No. 1", + "2": "Unpair Button No. 2", + "4": "Unpair Button No. 3" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 49, + "propertyName": "Button Pairing", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Button Pairing", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "Stop pairing", + "1": "Pair Button No. 1", + "2": "Pair Button No. 2", + "4": "Pair Button No. 3" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 96, + "propertyName": "Stop Playing Tone on Action Button", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Stop Playing Tone on Action Button", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 255, + "propertyName": "Reset to Factory Default Setting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Reset to Factory Default Setting", + "default": 0, + "min": 0, + "max": 1431655765, + "states": { + "1": "Resets all configuration parameters to default setting", + "1431655765": "Reset the product to factory default setting and exclude from Z-Wave network" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 96, + "commandClassName": "Multi Channel", + "property": "endpointIndizes", + "propertyName": "endpointIndizes", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": [1, 2, 3, 4, 5, 6, 7, 8] + }, + { + "endpoint": 1, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 1, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 6 + }, + { + "endpoint": 1, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Tone ID", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 1, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 1, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 17 + }, + { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 2, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 3, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 3, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 1 + }, + { + "endpoint": 3, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "24": "24CLOC~1 (10 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 3, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 3, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery maintenance status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "10": "Replace battery soon" + } + } + }, + { + "endpoint": 3, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 4, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 4, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 3 + }, + { + "endpoint": 4, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "24": "24CLOC~1 (10 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 4, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 4, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery maintenance status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "10": "Replace battery soon" + } + } + }, + { + "endpoint": 4, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 5, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 5, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 5 + }, + { + "endpoint": 5, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "24": "24CLOC~1 (10 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 5, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 5, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery maintenance status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "10": "Replace battery soon" + } + } + }, + { + "endpoint": 5, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 6, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 6, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 9 + }, + { + "endpoint": 6, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "24": "24CLOC~1 (10 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 6, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 6, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 7, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 7, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 18 + }, + { + "endpoint": 7, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "24": "24CLOC~1 (10 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 7, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 7, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + }, + { + "endpoint": 8, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 8, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Default tone ID", + "min": 0, + "max": 254 + }, + "value": 11 + }, + { + "endpoint": 8, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "toneId", + "propertyName": "toneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Play Tone", + "min": 0, + "max": 30, + "states": { + "0": "off", + "1": "01DING~1 (5 sec)", + "2": "02DING~1 (9 sec)", + "3": "03TRAD~1 (11 sec)", + "4": "04ELEC~1 (2 sec)", + "5": "05WEST~1 (13 sec)", + "6": "06CHIM~1 (7 sec)", + "7": "07CUCK~1 (31 sec)", + "8": "08TRAD~1 (6 sec)", + "9": "09SMOK~1 (11 sec)", + "10": "10SMOK~1 (6 sec)", + "11": "11FIRE~1 (35 sec)", + "12": "12COSE~1 (5 sec)", + "13": "13KLAX~1 (38 sec)", + "14": "14DEEP~1 (41 sec)", + "15": "15WARN~1 (37 sec)", + "16": "16TORN~1 (46 sec)", + "17": "17ALAR~1 (35 sec)", + "18": "18DEEP~1 (62 sec)", + "19": "19ALAR~1 (15 sec)", + "20": "20ALAR~1 (7 sec)", + "21": "21DIGI~1 (8 sec)", + "22": "22ALER~1 (64 sec)", + "23": "23SHIP~1 (4 sec)", + "24": "24CLOC~1 (10 sec)", + "25": "25CHRI~1 (4 sec)", + "26": "26GONG~1 (12 sec)", + "27": "27SING~1 (1 sec)", + "28": "28TONA~1 (5 sec)", + "29": "29UPWA~1 (2 sec)", + "30": "30DOOR~1 (27 sec)", + "255": "default" + } + } + }, + { + "endpoint": 8, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "volume", + "propertyName": "volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Volume", + "min": 0, + "max": 100, + "states": { + "0": "default" + }, + "unit": "%" + } + }, + { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Siren", + "propertyKey": "Siren status", + "propertyName": "Siren", + "propertyKeyName": "Siren status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Siren status", + "ccSpecific": { + "notificationType": 14 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Siren active" + } + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 3, + "label": "AV Control Point" + }, + "specific": { + "key": 1, + "label": "Sound Switch" + }, + "mandatorySupportedCCs": [ + 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 121, + "name": "Sound Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3" +} From 2740e56fd41f6d2c8d9993b7fa935e46bee2c1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 14 Jul 2021 19:58:02 +0200 Subject: [PATCH 288/818] Co2signal, set SCAN_INTERVAL (#53023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * limit co2signal, wip Signed-off-by: Daniel Hjelseth Høyer * limit co2signal Signed-off-by: Daniel Hjelseth Høyer * limit co2signal Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/co2signal/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 980ffa8549b..e9cfdb87983 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,4 +1,5 @@ """Support for the CO2signal platform.""" +from datetime import timedelta import logging import CO2Signal @@ -17,6 +18,7 @@ import homeassistant.helpers.config_validation as cv CONF_COUNTRY_CODE = "country_code" _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=3) ATTRIBUTION = "Data provided by CO2signal" From 49d109a969ca549bcab267285f30a613fcdf61a3 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 14 Jul 2021 19:59:11 +0200 Subject: [PATCH 289/818] Bump pypck to 0.7.10 (#53013) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 092e07eb5d2..1adc407d692 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.9"], + "requirements": ["pypck==0.7.10"], "codeowners": ["@alengwenus"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index da020c665f2..e8845193764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1667,7 +1667,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.9 +pypck==0.7.10 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87a084a3ece..f657da146d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -948,7 +948,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.9 +pypck==0.7.10 # homeassistant.components.plaato pyplaato==0.0.15 From 7e16d38fc859a9e8cda3053e6aee97a4a041b53a Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 14 Jul 2021 20:01:16 +0200 Subject: [PATCH 290/818] fix for timestamp not present in SIA (#53015) --- homeassistant/components/sia/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 66fdd7d95be..6b87c9cb1fc 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -6,6 +6,8 @@ from typing import Any from pysiaalarm import SIAEvent +from homeassistant.util.dt import utcnow + from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE PING_INTERVAL_MARGIN = 30 @@ -42,7 +44,9 @@ def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: "code": event.code, "message": event.message, "x_data": event.x_data, - "timestamp": event.timestamp.isoformat(), + "timestamp": event.timestamp.isoformat() + if event.timestamp + else utcnow().isoformat(), "event_qualifier": event.event_qualifier, "event_type": event.event_type, "partition": event.partition, From 4d711898c73a10e7a6bcbe70e0c4841cd20ca151 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 14 Jul 2021 14:04:04 -0400 Subject: [PATCH 291/818] Add missing test coverage for sirens (#53014) --- tests/components/siren/test_init.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index 6f90b8a9fcd..729990ceaeb 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -1,7 +1,10 @@ """The tests for the siren component.""" from unittest.mock import MagicMock -from homeassistant.components.siren import SirenEntity +import pytest + +from homeassistant.components.siren import SirenEntity, process_turn_on_params +from homeassistant.components.siren.const import SUPPORT_TONES class MockSirenEntity(SirenEntity): @@ -9,9 +12,10 @@ class MockSirenEntity(SirenEntity): _attr_is_on = True - def __init__(self, supported_features: int = 0) -> None: + def __init__(self, supported_features=0, available_tones=None): """Initialize mock siren entity.""" self._attr_supported_features = supported_features + self._attr_available_tones = available_tones async def test_sync_turn_on(hass): @@ -34,3 +38,19 @@ async def test_sync_turn_off(hass): await siren.async_turn_off() assert siren.turn_off.called + + +async def test_no_available_tones(hass): + """Test ValueError when siren advertises tones but has no available_tones.""" + siren = MockSirenEntity(SUPPORT_TONES) + siren.hass = hass + with pytest.raises(ValueError): + process_turn_on_params(siren, {"tone": "test"}) + + +async def test_missing_tones(hass): + """Test ValueError when setting a tone that is missing from available_tones.""" + siren = MockSirenEntity(SUPPORT_TONES, ["a", "b"]) + siren.hass = hass + with pytest.raises(ValueError): + process_turn_on_params(siren, {"tone": "test"}) From dd908caebade15adf061fade686355b94ed2f43a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 14 Jul 2021 14:14:36 -0400 Subject: [PATCH 292/818] Add zwave_js device triggers (#51968) * Initial support for zwave_js device triggers * lint * Add node status changed trigger * comments * create helper function and simplify trigger logic * simplify code * fix exception * remove unused type ignore * switch to append to make future changes easier * make exception consistent * Add state config schema validation * comment * remove 0 from falsy check * increase test coverage * typos * Add central scene and scene activation value notification triggers * reorder things for readability and enumerate node statuses * Add support for Basic CC value notifications * fix schemas since additional fields on triggers aren't very flexible * pylint * remove extra logger statement * fix comment * dont use get when we know key will be available in dict * tweak text * use better schema for required extra fields that are ints * rename trigger types to make them easier to parse * fix strings * missed renaming of one trigger type * typo * Fix strings * reduce complexity * Use Al's suggestion for strings * add additional failure test cases * remove errant logging statement * make CC required * raise vol.Invalid when value ID isn't legit to prepare for next PR * Use helper function * fix tests * black --- .../components/zwave_js/device_trigger.py | 371 ++++++++ homeassistant/components/zwave_js/helpers.py | 34 + .../components/zwave_js/strings.json | 8 + .../components/zwave_js/translations/en.json | 8 + tests/components/zwave_js/conftest.py | 14 + .../zwave_js/test_device_trigger.py | 831 ++++++++++++++++++ .../ge_in_wall_dimmer_switch_state.json | 642 ++++++++++++++ .../zwave_js/lock_schlage_be469_state.json | 63 +- 8 files changed, 1970 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zwave_js/device_trigger.py create mode 100644 tests/components/zwave_js/test_device_trigger.py create mode 100644 tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py new file mode 100644 index 00000000000..6734afd10e2 --- /dev/null +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -0,0 +1,371 @@ +"""Provides device triggers for Z-Wave JS.""" +from __future__ import annotations + +import voluptuous as vol +from zwave_js_server.const import CommandClass + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import event, state +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + entity_registry, +) +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_DATA_TYPE, + ATTR_ENDPOINT, + ATTR_EVENT, + ATTR_EVENT_LABEL, + ATTR_EVENT_TYPE, + ATTR_LABEL, + ATTR_PROPERTY, + ATTR_PROPERTY_KEY, + ATTR_TYPE, + ATTR_VALUE, + ATTR_VALUE_RAW, + DOMAIN, + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, +) +from .helpers import ( + async_get_node_from_device_id, + async_get_node_status_sensor_entity_id, + get_zwave_value_from_config, +) + +CONF_SUBTYPE = "subtype" +CONF_VALUE_ID = "value_id" + +# Trigger types +ENTRY_CONTROL_NOTIFICATION = "event.notification.entry_control" +NOTIFICATION_NOTIFICATION = "event.notification.notification" +BASIC_VALUE_NOTIFICATION = "event.value_notification.basic" +CENTRAL_SCENE_VALUE_NOTIFICATION = "event.value_notification.central_scene" +SCENE_ACTIVATION_VALUE_NOTIFICATION = "event.value_notification.scene_activation" +NODE_STATUS = "state.node_status" + +NOTIFICATION_EVENT_CC_MAPPINGS = ( + (ENTRY_CONTROL_NOTIFICATION, CommandClass.ENTRY_CONTROL), + (NOTIFICATION_NOTIFICATION, CommandClass.NOTIFICATION), +) + +# Event based trigger schemas +BASE_EVENT_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]), + } +) + +NOTIFICATION_NOTIFICATION_SCHEMA = BASE_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): NOTIFICATION_NOTIFICATION, + vol.Optional(f"{ATTR_TYPE}."): vol.Coerce(int), + vol.Optional(ATTR_LABEL): cv.string, + vol.Optional(ATTR_EVENT): vol.Coerce(int), + vol.Optional(ATTR_EVENT_LABEL): cv.string, + } +) + +ENTRY_CONTROL_NOTIFICATION_SCHEMA = BASE_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): ENTRY_CONTROL_NOTIFICATION, + vol.Optional(ATTR_EVENT_TYPE): vol.Coerce(int), + vol.Optional(ATTR_DATA_TYPE): vol.Coerce(int), + } +) + +BASE_VALUE_NOTIFICATION_EVENT_SCHEMA = BASE_EVENT_SCHEMA.extend( + { + vol.Required(ATTR_PROPERTY): vol.Any(int, str), + vol.Required(ATTR_PROPERTY_KEY): vol.Any(None, int, str), + vol.Required(ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(ATTR_VALUE): vol.Coerce(int), + vol.Required(CONF_SUBTYPE): cv.string, + } +) + +BASIC_VALUE_NOTIFICATION_SCHEMA = BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): BASIC_VALUE_NOTIFICATION, + } +) + +CENTRAL_SCENE_VALUE_NOTIFICATION_SCHEMA = BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): CENTRAL_SCENE_VALUE_NOTIFICATION, + } +) + +SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA = ( + BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SCENE_ACTIVATION_VALUE_NOTIFICATION, + } + ) +) + +# State based trigger schemas +BASE_STATE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + } +) + +NODE_STATUSES = ["asleep", "awake", "dead", "alive"] + +NODE_STATUS_SCHEMA = BASE_STATE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): NODE_STATUS, + vol.Optional(state.CONF_FROM): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_TO): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_FOR): cv.positive_time_period_dict, + } +) + +TRIGGER_SCHEMA = vol.Any( + ENTRY_CONTROL_NOTIFICATION_SCHEMA, + NOTIFICATION_NOTIFICATION_SCHEMA, + BASIC_VALUE_NOTIFICATION_SCHEMA, + CENTRAL_SCENE_VALUE_NOTIFICATION_SCHEMA, + SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA, + NODE_STATUS_SCHEMA, +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device triggers for Z-Wave JS devices.""" + dev_reg = device_registry.async_get(hass) + node = async_get_node_from_device_id(hass, device_id, dev_reg) + + triggers = [] + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + } + + # We can add a node status trigger if the node status sensor is enabled + ent_reg = entity_registry.async_get(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device_id, ent_reg, dev_reg + ) + if (entity := ent_reg.async_get(entity_id)) is not None and not entity.disabled: + triggers.append( + {**base_trigger, CONF_TYPE: NODE_STATUS, CONF_ENTITY_ID: entity_id} + ) + + # Handle notification event triggers + triggers.extend( + [ + {**base_trigger, CONF_TYPE: event_type, ATTR_COMMAND_CLASS: command_class} + for event_type, command_class in NOTIFICATION_EVENT_CC_MAPPINGS + if any(cc.id == command_class for cc in node.command_classes) + ] + ) + + # Handle central scene value notification event triggers + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: CENTRAL_SCENE_VALUE_NOTIFICATION, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_ENDPOINT: value.endpoint, + ATTR_COMMAND_CLASS: CommandClass.CENTRAL_SCENE, + CONF_SUBTYPE: f"Endpoint {value.endpoint} Scene {value.property_key}", + } + for value in node.get_command_class_values( + CommandClass.CENTRAL_SCENE + ).values() + if value.property_ == "scene" + ] + ) + + # Handle scene activation value notification event triggers + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: SCENE_ACTIVATION_VALUE_NOTIFICATION, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_ENDPOINT: value.endpoint, + ATTR_COMMAND_CLASS: CommandClass.SCENE_ACTIVATION, + CONF_SUBTYPE: f"Endpoint {value.endpoint}", + } + for value in node.get_command_class_values( + CommandClass.SCENE_ACTIVATION + ).values() + if value.property_ == "sceneId" + ] + ) + + # Handle basic value notification event triggers + # Nodes will only send Basic CC value notifications if a compatibility flag is set + if node.device_config.compat.get("treatBasicSetAsEvent", False): + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: BASIC_VALUE_NOTIFICATION, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_ENDPOINT: value.endpoint, + ATTR_COMMAND_CLASS: CommandClass.BASIC, + CONF_SUBTYPE: f"Endpoint {value.endpoint}", + } + for value in node.get_command_class_values(CommandClass.BASIC).values() + if value.property_ == "event" + ] + ) + + return triggers + + +def copy_available_params( + input_dict: dict, output_dict: dict, params: list[str] +) -> None: + """Copy available params from input into output.""" + for param in params: + if (val := input_dict.get(param)) not in ("", None): + output_dict[param] = val + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + trigger_type = config[CONF_TYPE] + trigger_platform = trigger_type.split(".")[0] + + event_data = {CONF_DEVICE_ID: config[CONF_DEVICE_ID]} + event_config = { + event.CONF_PLATFORM: "event", + event.CONF_EVENT_DATA: event_data, + } + + if ATTR_COMMAND_CLASS in config: + event_data[ATTR_COMMAND_CLASS] = config[ATTR_COMMAND_CLASS] + + # Take input data from automation trigger UI and add it to the trigger we are + # attaching to + if trigger_platform == "event": + if trigger_type == ENTRY_CONTROL_NOTIFICATION: + event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_NOTIFICATION_EVENT + copy_available_params(config, event_data, [ATTR_EVENT_TYPE, ATTR_DATA_TYPE]) + elif trigger_type == NOTIFICATION_NOTIFICATION: + event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_NOTIFICATION_EVENT + copy_available_params( + config, event_data, [ATTR_LABEL, ATTR_EVENT_LABEL, ATTR_EVENT] + ) + if (val := config.get(f"{ATTR_TYPE}.")) not in ("", None): + event_data[ATTR_TYPE] = val + elif trigger_type in ( + BASIC_VALUE_NOTIFICATION, + CENTRAL_SCENE_VALUE_NOTIFICATION, + SCENE_ACTIVATION_VALUE_NOTIFICATION, + ): + event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_VALUE_NOTIFICATION_EVENT + copy_available_params( + config, event_data, [ATTR_PROPERTY, ATTR_PROPERTY_KEY, ATTR_ENDPOINT] + ) + event_data[ATTR_VALUE_RAW] = config[ATTR_VALUE] + else: + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") + + event_config = event.TRIGGER_SCHEMA(event_config) + return await event.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + state_config = {state.CONF_PLATFORM: "state"} + + if trigger_platform == "state" and trigger_type == NODE_STATUS: + state_config[state.CONF_ENTITY_ID] = config[CONF_ENTITY_ID] + copy_available_params( + config, state_config, [state.CONF_FOR, state.CONF_FROM, state.CONF_TO] + ) + + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") + + +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List trigger capabilities.""" + node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID]) + value = ( + get_zwave_value_from_config(node, config) if ATTR_PROPERTY in config else None + ) + # Add additional fields to the automation trigger UI + if config[CONF_TYPE] == NOTIFICATION_NOTIFICATION: + return { + "extra_fields": vol.Schema( + { + vol.Optional(f"{ATTR_TYPE}."): cv.string, + vol.Optional(ATTR_LABEL): cv.string, + vol.Optional(ATTR_EVENT): cv.string, + vol.Optional(ATTR_EVENT_LABEL): cv.string, + } + ) + } + + if config[CONF_TYPE] == ENTRY_CONTROL_NOTIFICATION: + return { + "extra_fields": vol.Schema( + { + vol.Optional(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_DATA_TYPE): cv.string, + } + ) + } + + if config[CONF_TYPE] == NODE_STATUS: + return { + "extra_fields": vol.Schema( + { + vol.Optional(state.CONF_FROM): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_TO): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_FOR): cv.positive_time_period_dict, + } + ) + } + + if config[CONF_TYPE] in ( + BASIC_VALUE_NOTIFICATION, + CENTRAL_SCENE_VALUE_NOTIFICATION, + SCENE_ACTIVATION_VALUE_NOTIFICATION, + ): + if value.metadata.states: + value_schema = vol.In({int(k): v for k, v in value.metadata.states.items()}) + else: + value_schema = vol.All( + vol.Coerce(int), + vol.Range(min=value.metadata.min, max=value.metadata.max), + ) + + return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})} + + return {} diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 50d57d9a6e4..593d5ea4151 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -8,9 +8,11 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue, get_value_id +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import ( DeviceRegistry, async_get as async_get_dev_reg, @@ -175,3 +177,35 @@ def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveVal if value_id not in node.values: raise vol.Invalid(f"Value {value_id} can't be found on node {node}") return node.values[value_id] + + +@callback +def async_get_node_status_sensor_entity_id( + hass: HomeAssistant, + device_id: str, + ent_reg: EntityRegistry | None = None, + dev_reg: DeviceRegistry | None = None, +) -> str: + """Get the node status sensor entity ID for a given Z-Wave JS device.""" + if not ent_reg: + ent_reg = async_get_ent_reg(hass) + if not dev_reg: + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get(device_id) + if not device: + raise HomeAssistantError("Invalid Device ID provided") + + entry_id = next(entry_id for entry_id in device.config_entries) + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node = async_get_node_from_device_id(hass, device_id, dev_reg) + entity_id = ent_reg.async_get_entity_id( + SENSOR_DOMAIN, + DOMAIN, + f"{client.driver.controller.home_id}.{node.node_id}.node_status", + ) + if not entity_id: + raise HomeAssistantError( + "Node status sensor entity not found. Device may not be a zwave_js device" + ) + + return entity_id diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 1595ee58889..3d5aa277943 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -99,6 +99,14 @@ } }, "device_automation": { + "trigger_type": { + "event.notification.entry_control": "Sent an Entry Control notification", + "event.notification.notification": "Sent a notification", + "event.value_notification.basic": "Basic CC event on {subtype}", + "event.value_notification.central_scene": "Central Scene action on {subtype}", + "event.value_notification.scene_activation": "Scene Activation on {subtype}", + "state.node_status": "Node status changed" + }, "condition_type": { "node_status": "Node status", "config_parameter": "Config parameter {subtype} value", diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index bec90dd50d8..b742a011d19 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -56,6 +56,14 @@ "config_parameter": "Config parameter {subtype} value", "node_status": "Node status", "value": "Current value of a Z-Wave Value" + }, + "trigger_type": { + "event.notification.entry_control": "Sent an Entry Control notification", + "event.notification.notification": "Sent a notification", + "event.value_notification.basic": "Basic CC event on {subtype}", + "event.value_notification.central_scene": "Central Scene action on {subtype}", + "event.value_notification.scene_activation": "Scene Activation on {subtype}", + "state.node_status": "Node status changed" } }, "options": { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index f62c7fb8b9f..02d5b10cbba 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -429,6 +429,12 @@ def wallmote_central_scene_state_fixture(): return json.loads(load_fixture("zwave_js/wallmote_central_scene_state.json")) +@pytest.fixture(name="ge_in_wall_dimmer_switch_state", scope="session") +def ge_in_wall_dimmer_switch_state_fixture(): + """Load the ge in-wall dimmer switch node state fixture data.""" + return json.loads(load_fixture("zwave_js/ge_in_wall_dimmer_switch_state.json")) + + @pytest.fixture(name="aeotec_zw164_siren_state", scope="session") def aeotec_zw164_siren_state_fixture(): """Load the aeotec zw164 siren node state fixture data.""" @@ -795,6 +801,14 @@ def wallmote_central_scene_fixture(client, wallmote_central_scene_state): return node +@pytest.fixture(name="ge_in_wall_dimmer_switch") +def ge_in_wall_dimmer_switch_fixture(client, ge_in_wall_dimmer_switch_state): + """Mock a ge in-wall dimmer switch scene node.""" + node = Node(client, copy.deepcopy(ge_in_wall_dimmer_switch_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="aeotec_zw164_siren") def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): """Mock a wallmote central scene node.""" diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py new file mode 100644 index 00000000000..2c4d8ce2b33 --- /dev/null +++ b/tests/components/zwave_js/test_device_trigger.py @@ -0,0 +1,831 @@ +"""The tests for Z-Wave JS device triggers.""" +from unittest.mock import patch + +import pytest +import voluptuous_serialize +from zwave_js_server.const import CommandClass +from zwave_js_server.event import Event +from zwave_js_server.model.node import Node + +from homeassistant.components import automation +from homeassistant.components.zwave_js import DOMAIN, device_trigger +from homeassistant.components.zwave_js.device_trigger import ( + async_attach_trigger, + async_get_trigger_capabilities, +) +from homeassistant.components.zwave_js.helpers import ( + async_get_node_status_sensor_entity_id, +) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.setup import async_setup_component + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_notification_notification_triggers( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected triggers from a zwave_js device with the Notification CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.notification.notification", + "device_id": device.id, + "command_class": CommandClass.NOTIFICATION, + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_notification_notification_fires( + hass, client, lock_schlage_be469, integration, calls +): + """Test for event.notification.notification trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.notification", + "command_class": CommandClass.NOTIFICATION.value, + "type.": 6, + "event": 5, + "label": "Access Control", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.notification.notification - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Notification CC notification + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": node.node_id, + "ccId": 113, + "args": { + "type": 6, + "event": 5, + "label": "Access Control", + "eventLabel": "Keypad lock operation", + "parameters": {"userId": 1}, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.notification.notification - device - zwave_js_notification - {}".format( + CommandClass.NOTIFICATION + ) + + +async def test_get_trigger_capabilities_notification_notification( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a notification.notification trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.notification", + "command_class": CommandClass.NOTIFICATION.value, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert_lists_same( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ), + [ + {"name": "type.", "optional": True, "type": "string"}, + {"name": "label", "optional": True, "type": "string"}, + {"name": "event", "optional": True, "type": "string"}, + {"name": "event_label", "optional": True, "type": "string"}, + ], + ) + + +async def test_if_entry_control_notification_fires( + hass, client, lock_schlage_be469, integration, calls +): + """Test for notification.entry_control trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.entry_control", + "command_class": CommandClass.ENTRY_CONTROL.value, + "event_type": 5, + "data_type": 2, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.notification.notification - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Entry Control CC notification + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": node.node_id, + "ccId": 111, + "args": {"eventType": 5, "dataType": 2, "eventData": "555"}, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.notification.notification - device - zwave_js_notification - {}".format( + CommandClass.ENTRY_CONTROL + ) + + +async def test_get_trigger_capabilities_entry_control_notification( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a notification.entry_control trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.entry_control", + "command_class": CommandClass.ENTRY_CONTROL.value, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert_lists_same( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ), + [ + {"name": "event_type", "optional": True, "type": "string"}, + {"name": "data_type", "optional": True, "type": "string"}, + ], + ) + + +async def test_get_node_status_triggers(hass, client, lock_schlage_be469, integration): + """Test we get the expected triggers from a device with node status sensor enabled.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) + ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "state.node_status", + "device_id": device.id, + "entity_id": entity_id, + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_node_status_change_fires( + hass, client, lock_schlage_be469, integration, calls +): + """Test for node_status trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) + ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity_id, + "type": "state.node_status", + "from": "alive", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "state.node_status - " + "{{ trigger.platform}} - " + "{{ trigger.from_state.state }}" + ) + }, + }, + }, + ] + }, + ) + + # Test status change + event = Event( + "dead", data={"source": "node", "event": "dead", "nodeId": node.node_id} + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "state.node_status - device - alive" + + +async def test_get_trigger_capabilities_node_status( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a node_status trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) + ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity_id, + "type": "state.node_status", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "from", + "optional": True, + "options": [ + ("asleep", "asleep"), + ("awake", "awake"), + ("dead", "dead"), + ("alive", "alive"), + ], + "type": "select", + }, + { + "name": "to", + "optional": True, + "options": [ + ("asleep", "asleep"), + ("awake", "awake"), + ("dead", "dead"), + ("alive", "alive"), + ], + "type": "select", + }, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + ] + + +async def test_get_basic_value_notification_triggers( + hass, client, ge_in_wall_dimmer_switch, integration +): + """Test we get the expected triggers from a zwave_js device with the Basic CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_basic_value_notification_fires( + hass, client, ge_in_wall_dimmer_switch, integration, calls +): + """Test for event.value_notification.basic trigger firing.""" + node: Node = ge_in_wall_dimmer_switch + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC.value, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + "value": 0, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.basic - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Basic CC value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": node.node_id, + "args": { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "event", + "propertyName": "event", + "value": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "label": "Event value", + "min": 0, + "max": 255, + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.value_notification.basic - device - zwave_js_value_notification - {}".format( + CommandClass.BASIC + ) + + +async def test_get_trigger_capabilities_basic_value_notification( + hass, client, ge_in_wall_dimmer_switch, integration +): + """Test we get the expected capabilities from a value_notification.basic trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC.value, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "value", + "required": True, + "type": "integer", + "valueMin": 0, + "valueMax": 255, + } + ] + + +async def test_get_central_scene_value_notification_triggers( + hass, client, wallmote_central_scene, integration +): + """Test we get the expected triggers from a zwave_js device with the Central Scene CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.central_scene", + "device_id": device.id, + "command_class": CommandClass.CENTRAL_SCENE, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_central_scene_value_notification_fires( + hass, client, wallmote_central_scene, integration, calls +): + """Test for event.value_notification.central_scene trigger firing.""" + node: Node = wallmote_central_scene + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.value_notification.central_scene", + "command_class": CommandClass.CENTRAL_SCENE.value, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + "value": 0, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.central_scene - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Central Scene CC value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": node.node_id, + "args": { + "commandClassName": "Central Scene", + "commandClass": 91, + "endpoint": 0, + "property": "scene", + "propertyName": "scene", + "propertyKey": "001", + "propertyKey": "001", + "value": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Scene 004", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + }, + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.value_notification.central_scene - device - zwave_js_value_notification - {}".format( + CommandClass.CENTRAL_SCENE + ) + + +async def test_get_trigger_capabilities_central_scene_value_notification( + hass, client, wallmote_central_scene, integration +): + """Test we get the expected capabilities from a value_notification.central_scene trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.central_scene", + "device_id": device.id, + "command_class": CommandClass.CENTRAL_SCENE.value, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "value", + "required": True, + "type": "select", + "options": [(0, "KeyPressed"), (1, "KeyReleased"), (2, "KeyHeldDown")], + }, + ] + + +async def test_get_scene_activation_value_notification_triggers( + hass, client, hank_binary_switch, integration +): + """Test we get the expected triggers from a zwave_js device with the SceneActivation CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.scene_activation", + "device_id": device.id, + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_scene_activation_value_notification_fires( + hass, client, hank_binary_switch, integration, calls +): + """Test for event.value_notification.scene_activation trigger firing.""" + node: Node = hank_binary_switch + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.value_notification.scene_activation", + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + "value": 1, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.scene_activation - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Scene Activation CC value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": node.node_id, + "args": { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "value": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 1, + "max": 255, + "label": "Scene ID", + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.value_notification.scene_activation - device - zwave_js_value_notification - {}".format( + CommandClass.SCENE_ACTIVATION + ) + + +async def test_get_trigger_capabilities_scene_activation_value_notification( + hass, client, hank_binary_switch, integration +): + """Test we get the expected capabilities from a value_notification.scene_activation trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.scene_activation", + "device_id": device.id, + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "value", + "required": True, + "type": "integer", + "valueMin": 1, + "valueMax": 255, + } + ] + + +async def test_failure_scenarios(hass, client, hank_binary_switch, integration): + """Test failure scenarios.""" + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, {"type": "failed.test", "device_id": "invalid_device_id"}, None, {} + ) + + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, + {"type": "event.failed_type", "device_id": "invalid_device_id"}, + None, + {}, + ) + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, {"type": "failed.test", "device_id": device.id}, None, {} + ) + + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, + {"type": "event.failed_type", "device_id": device.id}, + None, + {}, + ) + + with patch( + "homeassistant.components.zwave_js.device_trigger.async_get_node_from_device_id", + return_value=None, + ), patch( + "homeassistant.components.zwave_js.helpers.get_zwave_value_from_config", + return_value=None, + ): + assert ( + await async_get_trigger_capabilities( + hass, {"type": "failed.test", "device_id": "invalid_device_id"} + ) + == {} + ) + + with pytest.raises(HomeAssistantError): + async_get_node_status_sensor_entity_id(hass, "invalid_device_id") diff --git a/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json b/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json new file mode 100644 index 00000000000..58d3f0d06ec --- /dev/null +++ b/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json @@ -0,0 +1,642 @@ +{ + "nodeId": 2, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 99, + "productId": 12344, + "productType": 18756, + "firmwareVersion": "5.26", + "zwavePlusVersion": 1, + "name": "LivingRoomLight", + "location": "LivingRoom", + "deviceConfig": { + "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0063/ge_14294_zw3005.json", + "manufacturer": "GE/Jasco", + "manufacturerId": 99, + "label": "14294 / ZW3005", + "description": "In-Wall Dimmer Switch", + "devices": [ + { + "productType": 18756, + "productId": 12344 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "treatBasicSetAsEvent": true + }, + "isEmbedded": true + }, + "label": "14294 / ZW3005", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 1, + "label": "Multilevel Power Switch" + }, + "mandatorySupportedCCs": [32, 38, 39], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "event", + "propertyName": "event", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Event value", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "min": 1, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Night Light", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Defines the behavior of the blue LED. Default is on when switch is off.", + "label": "Night Light", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "LED on when switch is OFF", + "1": "LED on when switch is ON", + "2": "LED always off" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Invert Switch", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Invert the ON/OFF Switch State.", + "label": "Invert Switch", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "No", + "1": "Yes" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Dim Rate Steps (Z-Wave Command)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of steps or levels", + "label": "Dim Rate Steps (Z-Wave Command)", + "default": 1, + "min": 0, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Dim Rate Timing (Z-Wave)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Timing of steps or levels", + "label": "Dim Rate Timing (Z-Wave)", + "default": 3, + "min": 1, + "max": 255, + "unit": "10ms", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Dim Rate Steps (Manual)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of steps or levels", + "label": "Dim Rate Steps (Manual)", + "default": 1, + "min": 1, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Dim Rate Timing (Manual)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Timing of steps", + "label": "Dim Rate Timing (Manual)", + "default": 3, + "min": 1, + "max": 255, + "unit": "10ms", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Dim Rate Steps (All-On/All-Off)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of steps or levels", + "label": "Dim Rate Steps (All-On/All-Off)", + "default": 1, + "min": 1, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Dim Rate Timing (All-On/All-Off)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Timing of steps or levels", + "label": "Dim Rate Timing (All-On/All-Off)", + "default": 3, + "min": 1, + "max": 255, + "unit": "10ms", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 18756 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 12344 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.34" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["5.26"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 1, + "label": "Multilevel Power Switch" + }, + "mandatorySupportedCCs": [32, 38, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 32, + "name": "Basic", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26" +} diff --git a/tests/fixtures/zwave_js/lock_schlage_be469_state.json b/tests/fixtures/zwave_js/lock_schlage_be469_state.json index be1ddb9c3f0..f85a8e6b005 100644 --- a/tests/fixtures/zwave_js/lock_schlage_be469_state.json +++ b/tests/fixtures/zwave_js/lock_schlage_be469_state.json @@ -50,7 +50,68 @@ "index": 0 } ], - "commandClasses": [], + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], "values": [ { "commandClassName": "Door Lock", From db8db18b5401fb23df7bdfab7ce3a3b359429804 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 15 Jul 2021 00:09:25 +0000 Subject: [PATCH 293/818] [ci skip] Translation update --- homeassistant/components/huawei_lte/translations/de.json | 5 +++-- homeassistant/components/sonos/translations/de.json | 1 + homeassistant/components/sonos/translations/nl.json | 2 +- homeassistant/components/zha/translations/ru.json | 4 ++-- homeassistant/components/zwave_js/translations/de.json | 7 +++++++ homeassistant/components/zwave_js/translations/et.json | 8 ++++++++ 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index a979eeb89fe..a3b40d0c0ae 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Benutzername" }, - "description": "Gib die Zugangsdaten zum Ger\u00e4t ein. Die Angabe von Benutzername und Passwort ist optional, erm\u00f6glicht aber die Unterst\u00fctzung weiterer Integrationsfunktionen. Andererseits kann die Verwendung einer autorisierten Verbindung zu Problemen beim Zugriff auf die Web-Schnittstelle des Ger\u00e4ts von au\u00dferhalb des Home Assistant f\u00fchren, w\u00e4hrend die Integration aktiv ist, und umgekehrt.", + "description": "Gib die Zugangsdaten f\u00fcr das Ger\u00e4t ein.", "title": "Konfiguriere Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Name des Benachrichtigungsdienstes (\u00c4nderung erfordert Neustart)", "recipient": "SMS-Benachrichtigungsempf\u00e4nger", "track_new_devices": "Neue Ger\u00e4te verfolgen", - "track_wired_clients": "Kabelgebundene Netzwerk-Clients verfolgen" + "track_wired_clients": "Kabelgebundene Netzwerk-Clients verfolgen", + "unauthenticated_mode": "Nicht authentifizierter Modus (\u00c4nderung erfordert erneutes Laden)" } } } diff --git a/homeassistant/components/sonos/translations/de.json b/homeassistant/components/sonos/translations/de.json index 5d66c168116..3860d56387d 100644 --- a/homeassistant/components/sonos/translations/de.json +++ b/homeassistant/components/sonos/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden.", + "not_sonos_device": "Erkanntes Ger\u00e4t ist kein Sonos-Ger\u00e4t", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/sonos/translations/nl.json b/homeassistant/components/sonos/translations/nl.json index 2a8d5a7c56e..0147c5c5382 100644 --- a/homeassistant/components/sonos/translations/nl.json +++ b/homeassistant/components/sonos/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "not_sonos_device": "Ontdekt apparaat is geen Sonos-apparaat", + "not_sonos_device": "Gevonden apparaat is geen Sonos-apparaat", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 6f88ca16ae9..8644cdbc03b 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -41,8 +41,8 @@ "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { - "consider_unavailable_battery": "\u0412\u0440\u0435\u043c\u044f, \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", - "consider_unavailable_mains": "\u0412\u0440\u0435\u043c\u044f, \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", + "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index a5c637b51fa..e33ab1f3f2d 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Wert des Konfigurationsparameters {subtype}", + "node_status": "Status des Knotens", + "value": "Aktueller Wert eines Z-Wave-Wertes" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Die Discovery-Informationen des Z-Wave JS-Add-On konnten nicht abgerufen werden.", diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 0439ec7c245..522c145d6d5 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -56,6 +56,14 @@ "config_parameter": "Seadeparameeteri {subtype} v\u00e4\u00e4rtus", "node_status": "S\u00f5lme olek", "value": "Z-Wave Value praegune v\u00e4\u00e4rtus" + }, + "trigger_type": { + "event.notification.entry_control": "L\u00e4bip\u00e4\u00e4su kontrolli teavitus on saadetud", + "event.notification.notification": "Teavitus on saadetud", + "event.value_notification.basic": "CC p\u00f5his\u00fcndmus {subtype}", + "event.value_notification.central_scene": "Keskse stseeni tegevus {subtype}", + "event.value_notification.scene_activation": "Stseeni aktiveerimine saidil {subtype}", + "state.node_status": "S\u00f5lme olek muutus" } }, "options": { From fbad453c89d4a9a1332cf410addc4ec0c05f3f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 15 Jul 2021 06:44:57 +0200 Subject: [PATCH 294/818] Pylint 2.9.3 (#52972) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/asuswrt/router.py | 4 ++-- homeassistant/components/august/activity.py | 6 +++--- .../components/august/binary_sensor.py | 4 ++-- .../components/bluesound/media_player.py | 4 ++-- homeassistant/components/decora/light.py | 2 +- homeassistant/components/dynalite/__init__.py | 4 ++-- .../components/eight_sleep/__init__.py | 3 +-- homeassistant/components/emby/media_player.py | 6 ++---- .../components/enphase_envoy/sensor.py | 15 +++++++------- .../components/environment_canada/weather.py | 2 +- .../components/esphome/binary_sensor.py | 4 ---- homeassistant/components/esphome/camera.py | 4 ---- homeassistant/components/esphome/climate.py | 3 +-- homeassistant/components/esphome/cover.py | 3 +-- homeassistant/components/esphome/fan.py | 3 +-- homeassistant/components/esphome/light.py | 3 +-- homeassistant/components/esphome/number.py | 3 +-- homeassistant/components/esphome/sensor.py | 3 +-- homeassistant/components/esphome/switch.py | 3 +-- homeassistant/components/flume/__init__.py | 2 +- homeassistant/components/freebox/sensor.py | 14 ++++++------- homeassistant/components/fritz/switch.py | 8 ++++---- homeassistant/components/glances/sensor.py | 12 +++++------ homeassistant/components/google/__init__.py | 8 ++++---- .../components/google_assistant/smart_home.py | 4 ++-- .../components/google_pubsub/__init__.py | 4 +++- .../components/here_travel_time/sensor.py | 2 +- .../components/homekit/type_cameras.py | 4 ++-- homeassistant/components/hue/sensor_base.py | 4 ++-- homeassistant/components/ios/notify.py | 2 +- .../components/itunes/media_player.py | 2 +- homeassistant/components/knx/climate.py | 2 +- homeassistant/components/kulersky/light.py | 1 - .../components/lg_netcast/media_player.py | 3 ++- .../components/life360/device_tracker.py | 2 +- .../components/luftdaten/config_flow.py | 2 +- homeassistant/components/mobile_app/notify.py | 2 +- homeassistant/components/modern_forms/fan.py | 2 +- .../components/modern_forms/light.py | 2 +- homeassistant/components/mysensors/light.py | 2 ++ homeassistant/components/netatmo/select.py | 8 +++----- homeassistant/components/ombi/sensor.py | 6 +++--- .../components/openhardwaremonitor/sensor.py | 3 +-- homeassistant/components/ozw/entity.py | 4 ++-- .../components/ping/binary_sensor.py | 10 +++++++--- .../components/proximity/__init__.py | 6 +++--- .../components/ring/binary_sensor.py | 4 ++-- homeassistant/components/ring/sensor.py | 10 +++------- homeassistant/components/rpi_camera/camera.py | 10 +++++++--- homeassistant/components/sense/sensor.py | 3 +-- homeassistant/components/sentry/__init__.py | 1 - homeassistant/components/slack/notify.py | 3 +-- homeassistant/components/smappee/sensor.py | 16 +++++++-------- homeassistant/components/snmp/switch.py | 2 +- .../components/switcher_kis/sensor.py | 4 ++-- .../components/synology_dsm/binary_sensor.py | 16 ++++++--------- .../components/synology_dsm/sensor.py | 20 ++++++++----------- .../components/synology_dsm/switch.py | 4 ++-- homeassistant/components/tasmota/discovery.py | 2 +- homeassistant/components/tellstick/sensor.py | 3 +-- homeassistant/components/torque/sensor.py | 4 ++-- homeassistant/components/tuya/__init__.py | 2 +- homeassistant/components/vallox/__init__.py | 4 ++-- homeassistant/components/webostv/__init__.py | 4 ++-- homeassistant/components/withings/const.py | 2 +- .../components/workday/binary_sensor.py | 2 +- homeassistant/components/xiaomi_miio/fan.py | 4 ++-- homeassistant/components/xiaomi_miio/light.py | 6 ++---- .../components/xiaomi_miio/switch.py | 4 ++-- homeassistant/components/xmpp/notify.py | 4 +++- .../components/zha/core/channels/closures.py | 2 +- .../components/zha/core/channels/general.py | 2 +- .../zha/core/channels/homeautomation.py | 2 +- .../components/zha/core/channels/hvac.py | 2 +- .../components/zha/core/channels/lighting.py | 2 +- .../components/zha/core/channels/lightlink.py | 2 +- .../zha/core/channels/measurement.py | 2 +- .../components/zha/core/channels/protocol.py | 2 +- .../components/zha/core/channels/security.py | 2 +- .../zha/core/channels/smartenergy.py | 2 +- .../components/zha/core/registries.py | 2 +- homeassistant/components/zha/core/typing.py | 2 +- homeassistant/components/zha/device_action.py | 4 ++-- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zwave/__init__.py | 4 ++-- homeassistant/config.py | 2 +- homeassistant/core.py | 8 ++++---- homeassistant/helpers/event.py | 6 +++--- homeassistant/loader.py | 2 +- homeassistant/util/__init__.py | 3 --- homeassistant/util/dt.py | 4 ++-- requirements_test.txt | 2 +- 92 files changed, 182 insertions(+), 213 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 3c911d7712e..9d1bcb35c9e 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -299,9 +299,9 @@ class AsusWrtRouter: ) track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN) - for device_mac in self._devices: + for device_mac, device in self._devices.items(): dev_info = wrt_devices.get(device_mac) - self._devices[device_mac].update(dev_info, consider_home) + device.update(dev_info, consider_home) for device_mac, dev_info in wrt_devices.items(): if device_mac in self._devices: diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 402a2ccd610..77630b92511 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -61,9 +61,9 @@ class ActivityStream(AugustSubscriberMixin): """Cleanup any debounces.""" for debouncer in self._update_debounce.values(): debouncer.async_cancel() - for house_id in self._schedule_updates: - if self._schedule_updates[house_id] is not None: - self._schedule_updates[house_id]() + for house_id, updater in self._schedule_updates.items(): + if updater is not None: + updater() self._schedule_updates[house_id] = None def get_latest_device_activity(self, device_id, activity_types): diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 97faf444f3b..27a115a0823 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -112,10 +112,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AugustDoorBinarySensor(data, "door_open", door)) for doorbell in data.doorbells: - for sensor_type in SENSOR_TYPES_DOORBELL: + for sensor_type, sensor in SENSOR_TYPES_DOORBELL.items(): _LOGGER.debug( "Adding doorbell sensor class %s for %s", - SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS], + sensor[SENSOR_DEVICE_CLASS], doorbell.device_name, ) entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index dff45ca68bd..86d0be72bdc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -193,8 +193,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for player in target_players: await getattr(player, method["method"])(**params) - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] + for service, method in SERVICE_TO_METHOD.items(): + schema = method["schema"] hass.services.async_register( DOMAIN, service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 45c42c4bb1c..2564ff0cd9e 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -8,6 +8,7 @@ from bluepy.btle import BTLEException # pylint: disable=import-error import decora # pylint: disable=import-error import voluptuous as vol +from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, @@ -16,7 +17,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv -import homeassistant.util as util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 1ee609961cc..703ac1373ba 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -108,8 +108,8 @@ TEMPLATE_SCHEMA = vol.Schema({str: TEMPLATE_DATA_SCHEMA}) def validate_area(config: dict[str, Any]) -> dict[str, Any]: """Validate that template parameters are only used if area is using the relevant template.""" conf_set = set() - for template in DEFAULT_TEMPLATES: - for conf in DEFAULT_TEMPLATES[template]: + for configs in DEFAULT_TEMPLATES.values(): + for conf in configs: conf_set.add(conf) if config.get(CONF_TEMPLATE): for conf in DEFAULT_TEMPLATES[config[CONF_TEMPLATE]]: diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 4e16cd1087f..f839b3fcc74 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -148,8 +148,7 @@ async def async_setup(hass, config): sensors = [] binary_sensors = [] if eight.users: - for user in eight.users: - obj = eight.users[user] + for obj in eight.users.values(): for sensor in SENSORS: sensors.append(f"{obj.side}_{sensor}") binary_sensors.append(f"{obj.side}_presence") diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 5656a1f1486..c562cf400b6 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -86,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Handle devices which are added to Emby.""" new_devices = [] active_devices = [] - for dev_id in emby.devices: + for dev_id, dev in emby.devices.items(): active_devices.append(dev_id) if ( dev_id not in active_emby_devices @@ -96,9 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= active_emby_devices[dev_id] = new new_devices.append(new) - elif ( - dev_id in inactive_emby_devices and emby.devices[dev_id].state != "Off" - ): + elif dev_id in inactive_emby_devices and dev.state != "Off": add = inactive_emby_devices.pop(dev_id) active_emby_devices[dev_id] = add _LOGGER.debug("Showing %s, item: %s", dev_id, add) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 5ccb540efd0..3fab9e320dc 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -56,14 +56,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = data[NAME] entities = [] - for condition in SENSORS: - entity_name = "" + for condition, sensor in SENSORS.items(): if ( condition == "inverters" and coordinator.data.get("inverters_production") is not None ): for inverter in coordinator.data["inverters_production"]: - entity_name = f"{name} {SENSORS[condition][0]} {inverter}" + entity_name = f"{name} {sensor[0]} {inverter}" split_name = entity_name.split(" ") serial_number = split_name[-1] entities.append( @@ -73,8 +72,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name, config_entry.unique_id, serial_number, - SENSORS[condition][1], - SENSORS[condition][2], + sensor[1], + sensor[2], coordinator, ) ) @@ -83,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if isinstance(data, str) and "not available" in data: continue - entity_name = f"{name} {SENSORS[condition][0]}" + entity_name = f"{name} {sensor[0]}" entities.append( Envoy( condition, @@ -91,8 +90,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name, config_entry.unique_id, None, - SENSORS[condition][1], - SENSORS[condition][2], + sensor[1], + sensor[2], coordinator, ) ) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a4a8a02cee9..cf24146da14 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -28,7 +28,7 @@ from homeassistant.components.weather import ( ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt +from homeassistant.util import dt CONF_FORECAST = "forecast" CONF_ATTRIBUTION = "Data provided by Environment Canada" diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 44ed1806ed6..338f3787090 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -26,10 +26,6 @@ async def async_setup_entry( ) -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=no-member - - class EsphomeBinarySensor( EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity ): diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 34b6d90f4d4..e8f37c3d191 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -32,10 +32,6 @@ async def async_setup_entry( ) -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=no-member - - class EsphomeCamera(Camera, EsphomeBaseEntity[CameraInfo, CameraState]): """A camera implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 8715cb368c2..218f0fb319b 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -149,8 +149,7 @@ _PRESETS: EsphomeEnumMapper[ClimatePreset, str] = EsphomeEnumMapper( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity): diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index d0d89cf40ad..e055ffc5d03 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -40,8 +40,7 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 7052ee42861..6abce0914cb 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -58,8 +58,7 @@ _FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection, str] = EsphomeEnumMapper( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 0dd024832bb..ba968900ef0 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -50,8 +50,7 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 91000731483..1a90cdbeb24 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -36,8 +36,7 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 48e32809456..97cb5718903 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -60,8 +60,7 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method _STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMapper( diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index c2c88ee9376..218cd3905b0 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -29,8 +29,7 @@ async def async_setup_entry( # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking -# pylint: disable=invalid-overridden-method,no-member +# pylint: disable=invalid-overridden-method class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 4fc66d0ee70..1b441aa6ba5 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -47,7 +47,7 @@ def _setup_entry(hass: HomeAssistant, entry: ConfigEntry): flume_devices = FlumeDeviceList(flume_auth, http_session=http_session) except RequestException as ex: raise ConfigEntryNotReady from ex - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: raise ConfigEntryAuthFailed from ex return flume_auth, flume_devices, http_session diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 8c4e611827e..e68f7208538 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -50,25 +50,23 @@ async def async_setup_entry( ) ) - for sensor_key in CONNECTION_SENSORS: - entities.append( - FreeboxSensor(router, sensor_key, CONNECTION_SENSORS[sensor_key]) - ) + for sensor_key, sensor in CONNECTION_SENSORS.items(): + entities.append(FreeboxSensor(router, sensor_key, sensor)) - for sensor_key in CALL_SENSORS: - entities.append(FreeboxCallSensor(router, sensor_key, CALL_SENSORS[sensor_key])) + for sensor_key, sensor in CALL_SENSORS.items(): + entities.append(FreeboxCallSensor(router, sensor_key, sensor)) _LOGGER.debug("%s - %s - %s disk(s)", router.name, router.mac, len(router.disks)) for disk in router.disks.values(): for partition in disk["partitions"]: - for sensor_key in DISK_PARTITION_SENSORS: + for sensor_key, sensor in DISK_PARTITION_SENSORS.items(): entities.append( FreeboxDiskSensor( router, disk, partition, sensor_key, - DISK_PARTITION_SENSORS[sensor_key], + sensor, ) ) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 6e808598b77..238e65feacf 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -262,8 +262,8 @@ def wifi_entities_list( networks[i] = ssid return [ - FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, networks[net]) - for net in networks + FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, network_name) + for net, network_name in networks.items() ] @@ -428,8 +428,8 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): "NewPortMappingDescription": "description", } - for key in attributes_dict: - self._attributes[attributes_dict[key]] = self.port_mapping[key] + for key, attr in attributes_dict.items(): + self._attributes[attr] = self.port_mapping[key] async def _async_handle_port_switch_on_off(self, turn_on: bool) -> bool: diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 8306543f700..0e032de67be 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -25,9 +25,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): client, name, disk["mnt_point"], - SENSOR_TYPES[sensor_type][1], + sensor_details[1], sensor_type, - SENSOR_TYPES[sensor_type], + sensor_details, ) ) elif sensor_details[0] == "sensors": @@ -39,9 +39,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): client, name, sensor["label"], - SENSOR_TYPES[sensor_type][1], + sensor_details[1], sensor_type, - SENSOR_TYPES[sensor_type], + sensor_details, ) ) elif client.api.data[sensor_details[0]]: @@ -50,9 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): client, name, "", - SENSOR_TYPES[sensor_type][1], + sensor_details[1], sensor_type, - SENSOR_TYPES[sensor_type], + sensor_details, ) ) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index b46d48848da..b929c3c4c37 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -223,10 +223,10 @@ def setup(hass, config): def check_correct_scopes(token_file): """Check for the correct scopes in file.""" - tokenfile = open(token_file).read() - if "readonly" in tokenfile: - _LOGGER.warning("Please re-authenticate with Google") - return False + with open(token_file) as tokenfile: + if "readonly" in tokenfile.read(): + _LOGGER.warning("Please re-authenticate with Google") + return False return True diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 747dc234efe..2ec51561eeb 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -214,8 +214,8 @@ async def handle_devices_execute(hass, data, payload): execute_results = await asyncio.gather( *[ - _entity_execute(entities[entity_id], data, executions[entity_id]) - for entity_id in executions + _entity_execute(entities[entity_id], data, execution) + for entity_id, execution in executions.items() ] ) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 365c118e99e..514b919e877 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -59,7 +59,9 @@ def setup(hass: HomeAssistant, yaml_config: dict[str, Any]): service_principal_path ) - topic_path = publisher.topic_path(project_id, topic_name) + topic_path = publisher.topic_path( # pylint: disable=no-member + project_id, topic_name + ) encoder = DateTimeJSONEncoder() diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index c02456b2a3f..11fd19bd895 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -27,7 +27,7 @@ from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType -import homeassistant.util.dt as dt +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index e21ca189b4e..040c4017bb3 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -141,9 +141,9 @@ class Camera(HomeAccessory, PyhapCamera): def __init__(self, hass, driver, name, entity_id, aid, config): """Initialize a Camera accessory object.""" self._ffmpeg = hass.data[DATA_FFMPEG] - for config_key in CONFIG_DEFAULTS: + for config_key, conf in CONFIG_DEFAULTS.items(): if config_key not in config: - config[config_key] = CONFIG_DEFAULTS[config_key] + config[config_key] = conf max_fps = config[CONF_MAX_FPS] max_width = config[CONF_MAX_WIDTH] diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 824f8cf42dc..957565c54e9 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -160,8 +160,8 @@ class SensorManager: ) ) - for platform in to_add: - self._component_add_entities[platform](to_add[platform]) + for platform, value in to_add.items(): + self._component_add_entities[platform](value) class GenericHueSensor(GenericHueDevice, entity.Entity): diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 853fb0d479a..a096a43ac85 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -38,7 +38,7 @@ def log_rate_limits(hass, target, resp, level=20): rate_limits["successful"], rate_limits["maximum"], rate_limits["errors"], - str(resetsAtTime).split(".")[0], + str(resetsAtTime).split(".", maxsplit=1)[0], ) diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index 29ec7eb4558..2b531773d55 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -384,7 +384,7 @@ class ItunesDevice(MediaPlayerEntity): def media_next_track(self): """Send media_next command to media player.""" - response = self.client.next() # pylint: disable=not-callable + response = self.client.next() self.update_state(response) def media_previous_track(self): diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index c2e2a269b27..803ded55441 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -185,7 +185,7 @@ class KNXClimate(KnxEntity, ClimateEntity): f"{self._device.temperature.group_address_state}_" f"{self._device.target_temperature.group_address_state}_" f"{self._device.target_temperature.group_address}_" - f"{self._device._setpoint_shift.group_address}" # pylint: disable=protected-access + f"{self._device._setpoint_shift.group_address}" ) async def async_update(self) -> None: diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index fd907235b45..6e04dbdfcfd 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -142,7 +142,6 @@ class KulerskyLight(LightEntity): try: if not self._available: await self._light.connect() - # pylint: disable=invalid-name rgbw = await self._light.get_color() except pykulersky.PykulerskyException as exc: if self._available: diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 31316ca975b..5b5ce313689 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -139,7 +139,8 @@ class LgTVDevice(MediaPlayerEntity): self._sources = dict(zip(channel_names, channel_list)) # sort source names by the major channel number source_tuples = [ - (k, self._sources[k].find("major").text) for k in self._sources + (k, source.find("major").text) + for k, source in self._sources.items() ] sorted_sources = sorted( source_tuples, key=lambda channel: int(channel[1]) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 5403a483ffb..6697dd50893 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -189,7 +189,7 @@ class Life360Scanner: { ATTR_ENTITY_ID: f"{DEVICE_TRACKER_DOMAIN}.{dev_id}", ATTR_WAIT: str(last_seen - (prev_seen or self._started)).split( - "." + ".", maxsplit=1 )[0], }, ) diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 0df12fc8907..f13fcc831dc 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -81,7 +81,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._show_form({CONF_SENSOR_ID: "invalid_sensor"}) available_sensors = [ - x for x in luftdaten.values if luftdaten.values[x] is not None + x for x, x_values in luftdaten.values.items() if x_values is not None ] if available_sensors: diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 1acb9f25c0c..162ec8afeab 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -79,7 +79,7 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): rate_limits[ATTR_PUSH_RATE_LIMITS_SUCCESSFUL], rate_limits[ATTR_PUSH_RATE_LIMITS_MAXIMUM], rate_limits[ATTR_PUSH_RATE_LIMITS_ERRORS], - str(resetsAtTime).split(".")[0], + str(resetsAtTime).split(".", maxsplit=1)[0], ) diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 2668b26857b..db8f2e011a9 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry -import homeassistant.helpers.entity_platform as entity_platform +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index 2c8298f00da..ee30c0c489f 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -import homeassistant.helpers.entity_platform as entity_platform +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index b08d94cebb0..e1f4dd3d1e0 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -163,6 +163,8 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): if self.assumed_state: # optimistically assume that light has changed state + # pylint: disable=no-value-for-parameter + # https://github.com/PyCQA/pylint/issues/4546 self._hs = color_util.color_RGB_to_hs(*rgb) # type: ignore[assignment] self._white = white self._values[self.value_type] = hex_color diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 726ae919099..718d7e440b9 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -87,11 +87,9 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._attr_unique_id = f"{self._home_id}-schedule-select" - self._attr_current_option = ( - self._data._get_selected_schedule( # pylint: disable=protected-access - home_id=self._home_id - ).get("name") - ) + self._attr_current_option = self._data._get_selected_schedule( + home_id=self._home_id + ).get("name") self._attr_options = options async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index 8c08b026b28..c91cf429c94 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -22,10 +22,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ombi = hass.data[DOMAIN]["instance"] - for sensor in SENSOR_TYPES: + for sensor, sensor_val in SENSOR_TYPES.items(): sensor_label = sensor - sensor_type = SENSOR_TYPES[sensor]["type"] - sensor_icon = SENSOR_TYPES[sensor]["icon"] + sensor_type = sensor_val["type"] + sensor_icon = sensor_val["icon"] sensors.append(OmbiSensor(sensor_label, sensor_type, ombi, sensor_icon)) add_entities(sensors, True) diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 8d14074892d..8f43c1e5e9b 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -88,8 +88,7 @@ class OpenHardwareMonitorDevice(SensorEntity): array = self._data.data[OHM_CHILDREN] _attributes = {} - for path_index in range(0, len(self.path)): - path_number = self.path[path_index] + for path_index, path_number in enumerate(self.path): values = array[path_number] if path_index == len(self.path) - 1: diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index 305601a2333..d5cafa615df 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -83,9 +83,9 @@ class ZWaveDeviceEntityValues: return # Go through the possible values for this entity defined by the schema. - for name in self._values: + for name, name_value in self._values.items(): # Skip if it's already been added. - if self._values[name] is not None: + if name_value is not None: continue # Skip if the value doesn't match the schema. if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index cf2d8f7ed7a..0c82c9ff8c4 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -256,14 +256,18 @@ class PingDataSubProcess(PingData): ) if sys.platform == "win32": - match = WIN32_PING_MATCHER.search(str(out_data).split("\n")[-1]) + match = WIN32_PING_MATCHER.search( + str(out_data).rsplit("\n", maxsplit=1)[-1] + ) rtt_min, rtt_avg, rtt_max = match.groups() return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": ""} if "max/" not in str(out_data): - match = PING_MATCHER_BUSYBOX.search(str(out_data).split("\n")[-1]) + match = PING_MATCHER_BUSYBOX.search( + str(out_data).rsplit("\n", maxsplit=1)[-1] + ) rtt_min, rtt_avg, rtt_max = match.groups() return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": ""} - match = PING_MATCHER.search(str(out_data).split("\n")[-1]) + match = PING_MATCHER.search(str(out_data).rsplit("\n", maxsplit=1)[-1]) rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} except asyncio.TimeoutError: diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index de9d6247f9f..1840162f896 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -230,10 +230,10 @@ class Proximity(Entity): closest_device: str = None dist_to_zone: float = None - for device in distances_to_zone: - if not dist_to_zone or distances_to_zone[device] < dist_to_zone: + for device, zone in distances_to_zone.items(): + if not dist_to_zone or zone < dist_to_zone: closest_device = device - dist_to_zone = distances_to_zone[device] + dist_to_zone = zone # If the closest device is one of the other devices. if closest_device != entity: diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 28d686df06a..d2c412a691d 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -30,8 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for device_type in ("doorbots", "authorized_doorbots", "stickup_cams"): - for sensor_type in SENSOR_TYPES: - if device_type not in SENSOR_TYPES[sensor_type][1]: + for sensor_type, sensor in SENSOR_TYPES.items(): + if device_type not in sensor[1]: continue for device in devices[device_type]: diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index fb1c38fcbde..97fb8ec9d21 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -19,19 +19,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"): - for sensor_type in SENSOR_TYPES: - if device_type not in SENSOR_TYPES[sensor_type][1]: + for sensor_type, sensor in SENSOR_TYPES.items(): + if device_type not in sensor[1]: continue for device in devices[device_type]: if device_type == "battery" and device.battery_life is None: continue - sensors.append( - SENSOR_TYPES[sensor_type][6]( - config_entry.entry_id, device, sensor_type - ) - ) + sensors.append(sensor[6](config_entry.entry_id, device, sensor_type)) async_add_entities(sensors) diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 2d7edd83fed..070e861b3c9 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -26,9 +26,10 @@ _LOGGER = logging.getLogger(__name__) def kill_raspistill(*args): """Kill any previously running raspistill process..""" - subprocess.Popen( + with subprocess.Popen( ["killall", "raspistill"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) + ): + pass def setup_platform(hass, config, add_entities, discovery_info=None): @@ -116,7 +117,10 @@ class RaspberryCamera(Camera): cmd_args.append("-a") cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP])) - subprocess.Popen(cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + with subprocess.Popen( + cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ): + pass def camera_image(self): """Return raspistill image response.""" diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index d779870d37d..238b0b83cde 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -96,8 +96,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for i in range(len(data.active_voltage)): devices.append(SenseVoltageSensor(data, i, sense_monitor_id)) - for type_id in TRENDS_SENSOR_TYPES: - typ = TRENDS_SENSOR_TYPES[type_id] + for type_id, typ in TRENDS_SENSOR_TYPES.items(): for var in SENSOR_VARIANTS: name = typ.name sensor_type = typ.sensor_type diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 37941a0bcaa..c34bc2b350a 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -78,7 +78,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } - # pylint: disable=abstract-class-instantiated sentry_sdk.init( dsn=entry.data[CONF_DSN], environment=entry.options.get(CONF_ENVIRONMENT), diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index c2ca834b565..0eadc26075e 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -22,8 +22,7 @@ from homeassistant.components.notify import ( ) from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_ICON, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import aiohttp_client, config_validation as cv -import homeassistant.helpers.template as template +from homeassistant.helpers import aiohttp_client, config_validation as cv, template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 024845a08fc..75c5da85c34 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -149,38 +149,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for service_location in smappee_base.smappee.service_locations.values(): # Add all basic sensors (realtime values and aggregators) # Some are available in local only env - for sensor in TREND_SENSORS: - if not service_location.local_polling or TREND_SENSORS[sensor][5]: + for sensor, attributes in TREND_SENSORS.items(): + if not service_location.local_polling or attributes[5]: entities.append( SmappeeSensor( smappee_base=smappee_base, service_location=service_location, sensor=sensor, - attributes=TREND_SENSORS[sensor], + attributes=attributes, ) ) if service_location.has_reactive_value: - for reactive_sensor in REACTIVE_SENSORS: + for reactive_sensor, attributes in REACTIVE_SENSORS.items(): entities.append( SmappeeSensor( smappee_base=smappee_base, service_location=service_location, sensor=reactive_sensor, - attributes=REACTIVE_SENSORS[reactive_sensor], + attributes=attributes, ) ) # Add solar sensors (some are available in local only env) if service_location.has_solar_production: - for sensor in SOLAR_SENSORS: - if not service_location.local_polling or SOLAR_SENSORS[sensor][5]: + for sensor, attributes in SOLAR_SENSORS.items(): + if not service_location.local_polling or attributes[5]: entities.append( SmappeeSensor( smappee_base=smappee_base, service_location=service_location, sensor=sensor, - attributes=SOLAR_SENSORS[sensor], + attributes=attributes, ) ) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 7210c8e5fd3..bc7807cb6db 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -240,7 +240,7 @@ class SnmpSwitch(SwitchEntity): await self._set(command) # User set vartype Null, command must be an empty string elif self._vartype == "Null": - await self._set(Null)("") + await self._set("") # user did not set vartype but command is digit: defaulting to Integer # or user did set vartype else: diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 5b6b40a0e2d..2325d382b56 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -78,8 +78,8 @@ async def async_setup_platform( device_data = hass.data[DOMAIN][DATA_DEVICE] async_add_entities( - SwitcherSensorEntity(device_data, attribute, SENSORS[attribute]) - for attribute in SENSORS + SwitcherSensorEntity(device_data, attribute, sensor) + for attribute, sensor in SENSORS.items() ) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index e94dc1a94ac..5f27aa3b038 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -32,17 +32,13 @@ async def async_setup_entry( | SynoDSMUpgradeBinarySensor | SynoDSMStorageBinarySensor ] = [ - SynoDSMSecurityBinarySensor( - api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator - ) - for sensor_type in SECURITY_BINARY_SENSORS + SynoDSMSecurityBinarySensor(api, sensor_type, sensor, coordinator) + for sensor_type, sensor in SECURITY_BINARY_SENSORS.items() ] entities += [ - SynoDSMUpgradeBinarySensor( - api, sensor_type, UPGRADE_BINARY_SENSORS[sensor_type], coordinator - ) - for sensor_type in UPGRADE_BINARY_SENSORS + SynoDSMUpgradeBinarySensor(api, sensor_type, sensor, coordinator) + for sensor_type, sensor in UPGRADE_BINARY_SENSORS.items() ] # Handle all disks @@ -52,11 +48,11 @@ async def async_setup_entry( SynoDSMStorageBinarySensor( api, sensor_type, - STORAGE_DISK_BINARY_SENSORS[sensor_type], + sensor, coordinator, disk, ) - for sensor_type in STORAGE_DISK_BINARY_SENSORS + for sensor_type, sensor in STORAGE_DISK_BINARY_SENSORS.items() ] async_add_entities(entities) diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 4cf982e15f6..5942ce4a5b1 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -46,10 +46,8 @@ async def async_setup_entry( coordinator = data[COORDINATOR_CENTRAL] entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ - SynoDSMUtilSensor( - api, sensor_type, UTILISATION_SENSORS[sensor_type], coordinator - ) - for sensor_type in UTILISATION_SENSORS + SynoDSMUtilSensor(api, sensor_type, sensor, coordinator) + for sensor_type, sensor in UTILISATION_SENSORS.items() ] # Handle all volumes @@ -59,11 +57,11 @@ async def async_setup_entry( SynoDSMStorageSensor( api, sensor_type, - STORAGE_VOL_SENSORS[sensor_type], + sensor, coordinator, volume, ) - for sensor_type in STORAGE_VOL_SENSORS + for sensor_type, sensor in STORAGE_VOL_SENSORS.items() ] # Handle all disks @@ -73,18 +71,16 @@ async def async_setup_entry( SynoDSMStorageSensor( api, sensor_type, - STORAGE_DISK_SENSORS[sensor_type], + sensor, coordinator, disk, ) - for sensor_type in STORAGE_DISK_SENSORS + for sensor_type, sensor in STORAGE_DISK_SENSORS.items() ] entities += [ - SynoDSMInfoSensor( - api, sensor_type, INFORMATION_SENSORS[sensor_type], coordinator - ) - for sensor_type in INFORMATION_SENSORS + SynoDSMInfoSensor(api, sensor_type, sensor, coordinator) + for sensor_type, sensor in INFORMATION_SENSORS.items() ] async_add_entities(entities) diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 27ffbfde799..e08516ec03a 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -44,9 +44,9 @@ async def async_setup_entry( await coordinator.async_refresh() entities += [ SynoDSMSurveillanceHomeModeToggle( - api, sensor_type, SURVEILLANCE_SWITCH[sensor_type], version, coordinator + api, sensor_type, switch, version, coordinator ) - for sensor_type in SURVEILLANCE_SWITCH + for sensor_type, switch in SURVEILLANCE_SWITCH.items() ] async_add_entities(entities, True) diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 1e5bde5a3d5..37b373d30a1 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -18,7 +18,7 @@ from hatasmota.models import DiscoveryHashType, TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient from hatasmota.sensor import TasmotaBaseSensorConfig -import homeassistant.components.sensor as sensor +from homeassistant.components import sensor from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dev_reg diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 5be89216365..599c19388d6 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -127,9 +127,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: continue - for datatype in sensor_value_descriptions: + for datatype, sensor_info in sensor_value_descriptions.items(): if datatype & datatype_mask and tellcore_sensor.has_value(datatype): - sensor_info = sensor_value_descriptions[datatype] sensors.append( TellstickSensor(sensor_name, tellcore_sensor, datatype, sensor_info) ) diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 156259adccb..8e3053d9bd8 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -95,10 +95,10 @@ class TorqueReceiveDataView(HomeAssistantView): if pid in self.sensors: self.sensors[pid].async_on_update(data[key]) - for pid in names: + for pid, name in names.items(): if pid not in self.sensors: self.sensors[pid] = TorqueSensor( - ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]), units.get(pid) + ENTITY_NAME_FORMAT.format(self.vehicle, name), units.get(pid) ) hass.async_add_job(self.add_entities, [self.sensors[pid]]) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 595350324f9..7a639665948 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -197,7 +197,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_cancel_tuya_tracker(event): - domain_data[TUYA_TRACKER]() + domain_data[TUYA_TRACKER]() # pylint: disable=not-callable domain_data[STOP_CANCEL] = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_cancel_tuya_tracker diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index eb5edfe7fcf..27210e0c750 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -112,8 +112,8 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {"client": client, "state_proxy": state_proxy, "name": name} - for vallox_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[vallox_service]["schema"] + for vallox_service, method in SERVICE_TO_METHOD.items(): + schema = method["schema"] hass.services.async_register( DOMAIN, vallox_service, service_handler.async_handle, schema=schema ) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index af7f59bd266..96559fe5a68 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -92,8 +92,8 @@ async def async_setup(hass, config): data["method"] = method["method"] async_dispatcher_send(hass, DOMAIN, data) - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] + for service, method in SERVICE_TO_METHOD.items(): + schema = method["schema"] hass.services.async_register( DOMAIN, service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index d88f4e38c6a..dd744152bb1 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -1,7 +1,7 @@ """Constants used by the Withings component.""" from enum import Enum -import homeassistant.const as const +from homeassistant import const CONF_PROFILES = "profiles" CONF_USE_WEBHOOK = "use_webhook" diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index ed3822b9698..533bc77e27c 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME, WEEKDAYS import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index a5a5122ea07..49ae1cf7b7c 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -634,8 +634,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if update_tasks: await asyncio.wait(update_tasks) - for air_purifier_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[air_purifier_service].get( + for air_purifier_service, method in SERVICE_TO_METHOD.items(): + schema = method[air_purifier_service].get( "schema", AIRPURIFIER_SERVICE_SCHEMA ) hass.services.async_register( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index f6cd468ad00..6025ae047c6 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -241,10 +241,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if update_tasks: await asyncio.wait(update_tasks) - for xiaomi_miio_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( - "schema", XIAOMI_MIIO_SERVICE_SCHEMA - ) + for xiaomi_miio_service, method in SERVICE_TO_METHOD.items(): + schema = method.get("schema", XIAOMI_MIIO_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index ace0e52eaea..28c4f79ef28 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -250,8 +250,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if update_tasks: await asyncio.wait(update_tasks) - for plug_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA) + for plug_service, method in SERVICE_TO_METHOD.items(): + schema = method[plug_service].get("schema", SERVICE_SCHEMA) hass.services.async_register( DOMAIN, plug_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index bc5ebf12f75..29624f37ffa 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -190,7 +190,9 @@ async def async_send_message( # noqa: C901 _LOGGER.info("Sending file to %s", recipient) message = self.Message(sto=recipient, stype="chat") message["body"] = url - message["oob"]["url"] = url + message["oob"][ # pylint: disable=invalid-sequence-index + "url" + ] = url try: message.send() except (IqError, IqTimeout, XMPPError) as ex: diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index e427bc962b4..c63d069767d 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -1,5 +1,5 @@ """Closures channels module for Zigbee Home Automation.""" -import zigpy.zcl.clusters.closures as closures +from zigpy.zcl.clusters import closures from homeassistant.core import callback diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index a0c2c3bc699..5ca8c9fd4ba 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -6,7 +6,7 @@ from collections.abc import Coroutine from typing import Any import zigpy.exceptions -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general from zigpy.zcl.foundation import Status from homeassistant.core import callback diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 6e9d4138621..583cfb105bd 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Coroutine -import zigpy.zcl.clusters.homeautomation as homeautomation +from zigpy.zcl.clusters import homeautomation from .. import registries from ..const import ( diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 76f9c0b4e80..6b0cd9e5e28 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -11,7 +11,7 @@ from collections import namedtuple from typing import Any from zigpy.exceptions import ZigbeeException -import zigpy.zcl.clusters.hvac as hvac +from zigpy.zcl.clusters import hvac from zigpy.zcl.foundation import Status from homeassistant.core import callback diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 8c2b2bddd67..fbf53bec9a5 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Coroutine from contextlib import suppress -import zigpy.zcl.clusters.lighting as lighting +from zigpy.zcl.clusters import lighting from .. import registries from ..const import REPORT_CONFIG_DEFAULT diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index e42a3b20053..46c40fdaff0 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -2,7 +2,7 @@ import asyncio import zigpy.exceptions -import zigpy.zcl.clusters.lightlink as lightlink +from zigpy.zcl.clusters import lightlink from .. import registries from .base import ChannelStatus, ZigbeeChannel diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 77351441b56..19ecc8a6335 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -1,5 +1,5 @@ """Measurement channels module for Zigbee Home Automation.""" -import zigpy.zcl.clusters.measurement as measurement +from zigpy.zcl.clusters import measurement from .. import registries from ..const import ( diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index 4162809da7a..51d837a8014 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -1,5 +1,5 @@ """Protocol channels module for Zigbee Home Automation.""" -import zigpy.zcl.clusters.protocol as protocol +from zigpy.zcl.clusters import protocol from .. import registries from .base import ZigbeeChannel diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 2af44bdf4e1..cb90c740065 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -11,7 +11,7 @@ from collections.abc import Coroutine import logging from zigpy.exceptions import ZigbeeException -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import security from zigpy.zcl.clusters.security import IasAce as AceCluster from homeassistant.core import CALLABLE_T, callback diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index cfe395773ad..4e6302d32b5 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Coroutine -import zigpy.zcl.clusters.smartenergy as smartenergy +from zigpy.zcl.clusters import smartenergy from homeassistant.const import ( POWER_WATT, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 6bb9e155b0f..04e97f8b7ed 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -5,9 +5,9 @@ import collections from typing import Callable, Dict import attr +from zigpy import zcl import zigpy.profiles.zha import zigpy.profiles.zll -import zigpy.zcl as zcl from homeassistant.components.alarm_control_panel import DOMAIN as ALARM from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index 0fe46a4628e..15e8be0db1e 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -26,8 +26,8 @@ ZigpyGroupType = zigpy.group.Group ZigpyZdoType = zigpy.zdo.ZDO if TYPE_CHECKING: + from homeassistant.components.zha.core import channels import homeassistant.components.zha.core.channels - import homeassistant.components.zha.core.channels as channels import homeassistant.components.zha.core.channels.base as base_channels import homeassistant.components.zha.core.device import homeassistant.components.zha.core.gateway diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 9d419b16435..de39ff50511 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -67,8 +67,8 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: ] actions = [ action - for channel in DEVICE_ACTIONS - for action in DEVICE_ACTIONS[channel] + for channel, channel_actions in DEVICE_ACTIONS.items() + for action in channel_actions if channel in cluster_channels ] for action in actions: diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 3c50261b565..0176cf1ea3b 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -6,7 +6,7 @@ import functools import math from zigpy.exceptions import ZigbeeException -import zigpy.zcl.clusters.hvac as hvac +from zigpy.zcl.clusters import hvac from homeassistant.components.fan import ( ATTR_PERCENTAGE, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 6cf39709aaf..8a5705ae7bb 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -1114,8 +1114,8 @@ class ZWaveDeviceEntityValues: """ if not check_node_schema(value.node, self._schema): return - for name in self._values: - if self._values[name] is not None: + for name, name_value in self._values.items(): + if name_value is not None: continue if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): continue diff --git a/homeassistant/config.py b/homeassistant/config.py index c5f29f3c3c1..5a8f71bda77 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -899,7 +899,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: This method is a coroutine. """ # pylint: disable=import-outside-toplevel - import homeassistant.helpers.check_config as check_config + from homeassistant.helpers import check_config res = await check_config.async_check_ha_config_file(hass) diff --git a/homeassistant/core.py b/homeassistant/core.py index b9bf97e7e6c..0c2658952ce 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -201,7 +201,7 @@ class CoreState(enum.Enum): final_write = "FINAL_WRITE" stopped = "STOPPED" - def __str__(self) -> str: # pylint: disable=invalid-str-returned + def __str__(self) -> str: """Return the event.""" return self.value @@ -593,7 +593,7 @@ class EventOrigin(enum.Enum): local = "LOCAL" remote = "REMOTE" - def __str__(self) -> str: # pylint: disable=invalid-str-returned + def __str__(self) -> str: """Return the event.""" return self.value @@ -669,7 +669,7 @@ class EventBus: This method must be run in the event loop. """ - return {key: len(self._listeners[key]) for key in self._listeners} + return {key: len(listeners) for key, listeners in self._listeners.items()} @property def listeners(self) -> dict[str, int]: @@ -1298,7 +1298,7 @@ class ServiceRegistry: This method must be run in the event loop. """ - return {domain: self._services[domain].copy() for domain in self._services} + return {domain: service.copy() for domain, service in self._services.items()} def has_service(self, domain: str, service: str) -> bool: """Test if specified service exists. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 1fe542c096c..ab54d159f5e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -977,10 +977,10 @@ class _TrackTemplateResultInfo: self._track_state_changes.async_update_listeners( _render_infos_to_track_states( [ - _suppress_domain_all_in_render_info(self._info[template]) + _suppress_domain_all_in_render_info(info) if self._rate_limit.async_has_timer(template) - else self._info[template] - for template in self._info + else info + for template, info in self._info.items() ] ) ) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 06bf5045c9f..a535db4bde2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -534,7 +534,7 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration try: integration = await _async_get_integration(hass, domain) - except Exception: # pylint: disable=broad-except + except Exception: # Remove event from cache. cache.pop(domain) event.set() diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index f7f07434555..bf11103b3fa 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -161,9 +161,6 @@ def get_random_string(length: int = 10) -> str: class OrderedEnum(enum.Enum): """Taken from Python 3.4.0 docs.""" - # https://github.com/PyCQA/pylint/issues/2306 - # pylint: disable=comparison-with-callable - def __ge__(self, other: ENUM_T) -> bool: """Return the greater than element.""" if self.__class__ is other.__class__: diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index a9a6ca4e3a3..93737ce0c3d 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -13,9 +13,9 @@ import ciso8601 from homeassistant.const import MATCH_ALL if sys.version_info[:2] >= (3, 9): - import zoneinfo # pylint: disable=import-error + import zoneinfo else: - from backports import zoneinfo # pylint: disable=import-error + from backports import zoneinfo DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc diff --git a/requirements_test.txt b/requirements_test.txt index 660ea2a11fd..8b6ab238037 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.902 pre-commit==2.13.0 -pylint==2.8.3 +pylint==2.9.3 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From f15236994436bc6b92ed813feb5f5d6f26f567b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 15 Jul 2021 06:47:24 +0200 Subject: [PATCH 295/818] Use entity class attributes for Co2signal (#53032) --- homeassistant/components/co2signal/sensor.py | 32 +++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index e9cfdb87983..cb2ff40c062 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -47,10 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Setting up the sensor using the %s", country_code) - devs = [] - - devs.append(CO2Sensor(token, country_code, lat, lon)) - add_entities(devs, True) + add_entities([CO2Sensor(token, country_code, lat, lon)], True) class CO2Sensor(SensorEntity): @@ -65,42 +62,27 @@ class CO2Sensor(SensorEntity): self._country_code = country_code self._latitude = lat self._longitude = lon - self._data = None if country_code is not None: device_name = country_code else: device_name = f"{round(self._latitude, 2)}/{round(self._longitude, 2)}" - self._friendly_name = f"CO2 intensity - {device_name}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._friendly_name - - @property - def state(self): - """Return the state of the device.""" - return self._data - - @property - def extra_state_attributes(self): - """Return the state attributes of the last update.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_name = f"CO2 intensity - {device_name}" + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest data and updates the states.""" - _LOGGER.debug("Update data for %s", self._friendly_name) + _LOGGER.debug("Update data for %s", self.name) if self._country_code is not None: - self._data = CO2Signal.get_latest_carbon_intensity( + data = CO2Signal.get_latest_carbon_intensity( self._token, country_code=self._country_code ) else: - self._data = CO2Signal.get_latest_carbon_intensity( + data = CO2Signal.get_latest_carbon_intensity( self._token, latitude=self._latitude, longitude=self._longitude ) - self._data = round(self._data, 2) + self._attr_state = round(data, 2) From db97fd3d5b2ba78af30bffcaa2fa41a61b4e60eb Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Thu, 15 Jul 2021 05:50:23 +0100 Subject: [PATCH 296/818] Support user-defined base currency for Coinbase exchange rate sensors (#52879) --- homeassistant/components/coinbase/__init__.py | 19 +++++++++++-------- .../components/coinbase/config_flow.py | 9 +++++++++ homeassistant/components/coinbase/const.py | 1 + homeassistant/components/coinbase/sensor.py | 8 ++++---- .../components/coinbase/strings.json | 5 +++-- .../components/coinbase/translations/en.json | 5 ++--- 6 files changed, 30 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 08b97756dff..033c398e09c 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -20,6 +20,7 @@ from .const import ( API_ACCOUNT_ID, API_ACCOUNTS_DATA, CONF_CURRENCIES, + CONF_EXCHANGE_BASE, CONF_EXCHANGE_RATES, CONF_YAML_API_TOKEN, DOMAIN, @@ -67,9 +68,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coinbase from a config entry.""" - instance = await hass.async_add_executor_job( - create_and_update_instance, entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN] - ) + instance = await hass.async_add_executor_job(create_and_update_instance, entry) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -91,10 +90,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -def create_and_update_instance(api_key, api_token): +def create_and_update_instance(entry: ConfigEntry) -> CoinbaseData: """Create and update a Coinbase Data instance.""" - client = Client(api_key, api_token) - instance = CoinbaseData(client) + client = Client(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]) + base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD") + instance = CoinbaseData(client, base_rate) instance.update() return instance @@ -139,11 +139,12 @@ def get_accounts(client): class CoinbaseData: """Get the latest data and update the states.""" - def __init__(self, client): + def __init__(self, client, exchange_base): """Init the coinbase data object.""" self.client = client self.accounts = None + self.exchange_base = exchange_base self.exchange_rates = None self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] @@ -153,7 +154,9 @@ class CoinbaseData: try: self.accounts = get_accounts(self.client) - self.exchange_rates = self.client.get_exchange_rates() + self.exchange_rates = self.client.get_exchange_rates( + currency=self.exchange_base + ) except AuthenticationError as coinbase_error: _LOGGER.error( "Authentication error connecting to coinbase: %s", coinbase_error diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index adfa9977518..4ea36dad266 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -15,6 +15,7 @@ from .const import ( API_ACCOUNT_CURRENCY, API_RATES, CONF_CURRENCIES, + CONF_EXCHANGE_BASE, CONF_EXCHANGE_RATES, CONF_OPTIONS, CONF_YAML_API_TOKEN, @@ -156,6 +157,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): errors = {} default_currencies = self.config_entry.options.get(CONF_CURRENCIES, []) default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES, []) + default_exchange_base = self.config_entry.options.get(CONF_EXCHANGE_BASE, "USD") if user_input is not None: # Pass back user selected options, even if bad @@ -165,6 +167,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if CONF_EXCHANGE_RATES in user_input: default_exchange_rates = user_input[CONF_EXCHANGE_RATES] + if CONF_EXCHANGE_RATES in user_input: + default_exchange_base = user_input[CONF_EXCHANGE_BASE] + try: await validate_options(self.hass, self.config_entry, user_input) except CurrencyUnavaliable: @@ -189,6 +194,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_EXCHANGE_RATES, default=default_exchange_rates, ): cv.multi_select(RATES), + vol.Optional( + CONF_EXCHANGE_BASE, + default=default_exchange_base, + ): vol.In(WALLETS), } ), errors=errors, diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 035706c46ce..a7ed0b15986 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -1,6 +1,7 @@ """Constants used for Coinbase.""" CONF_CURRENCIES = "account_balance_currencies" +CONF_EXCHANGE_BASE = "exchange_base" CONF_EXCHANGE_RATES = "exchange_rate_currencies" CONF_OPTIONS = "options" DOMAIN = "coinbase" diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 13981619051..c86f21bac1d 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if CONF_CURRENCIES in config_entry.options: desired_currencies = config_entry.options[CONF_CURRENCIES] - exchange_native_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY] + exchange_base_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY] for currency in desired_currencies: if currency not in provided_currencies: @@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ExchangeRateSensor( instance, rate, - exchange_native_currency, + exchange_base_currency, ) ) @@ -149,7 +149,7 @@ class AccountSensor(SensorEntity): class ExchangeRateSensor(SensorEntity): """Representation of a Coinbase.com sensor.""" - def __init__(self, coinbase_data, exchange_currency, native_currency): + def __init__(self, coinbase_data, exchange_currency, exchange_base): """Initialize the sensor.""" self._coinbase_data = coinbase_data self.currency = exchange_currency @@ -158,7 +158,7 @@ class ExchangeRateSensor(SensorEntity): self._state = round( 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) - self._unit_of_measurement = native_currency + self._unit_of_measurement = exchange_base @property def name(self): diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json index 399bfbd894a..ce80db35918 100644 --- a/homeassistant/components/coinbase/strings.json +++ b/homeassistant/components/coinbase/strings.json @@ -25,7 +25,8 @@ "description": "Adjust Coinbase Options", "data": { "account_balance_currencies": "Wallet balances to report.", - "exchange_rate_currencies": "Exchange rates to report." + "exchange_rate_currencies": "Exchange rates to report.", + "exchange_base": "Base currency for exchange rate sensors." } } }, @@ -35,4 +36,4 @@ "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index 12db6bf8a30..e20f5f2e264 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -12,9 +12,7 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret", - "currencies": "Account Balance Currencies", - "exchange_rates": "Exchange Rates" + "api_token": "API Secret" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" @@ -31,6 +29,7 @@ "init": { "data": { "account_balance_currencies": "Wallet balances to report.", + "exchange_base": "Base currency for exchange rate sensors.", "exchange_rate_currencies": "Exchange rates to report." }, "description": "Adjust Coinbase Options" From a5cdc0157bd5f3ccdfd617f0e471f804cd20b416 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Jul 2021 08:31:17 +0200 Subject: [PATCH 297/818] Remove deprecated YAML configuration from Buienradar (#52939) --- .../components/buienradar/__init__.py | 118 +----------------- homeassistant/components/buienradar/camera.py | 26 +--- .../components/buienradar/config_flow.py | 15 --- homeassistant/components/buienradar/const.py | 5 - homeassistant/components/buienradar/sensor.py | 30 +---- .../components/buienradar/weather.py | 19 --- .../components/buienradar/test_config_flow.py | 28 ----- tests/components/buienradar/test_init.py | 89 ------------- 8 files changed, 6 insertions(+), 324 deletions(-) diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index 0474876bf2f..d7ec47d2bf8 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -1,46 +1,17 @@ """The buienradar integration.""" from __future__ import annotations -import logging - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry -from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_COUNTRY, - CONF_DELTA, - CONF_DIMENSION, - CONF_TIMEFRAME, - DEFAULT_COUNTRY, - DEFAULT_DELTA, - DEFAULT_DIMENSION, - DEFAULT_TIMEFRAME, - DOMAIN, -) +from .const import DOMAIN PLATFORMS = ["camera", "sensor", "weather"] -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the buienradar component.""" - hass.data.setdefault(DOMAIN, {}) - - weather_configs = _filter_domain_configs(config, "weather", DOMAIN) - sensor_configs = _filter_domain_configs(config, "sensor", DOMAIN) - camera_configs = _filter_domain_configs(config, "camera", DOMAIN) - - _import_configs(hass, weather_configs, sensor_configs, camera_configs) - - return True - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up buienradar from a config entry.""" + hass.data.setdefault(DOMAIN, {}) hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True @@ -56,86 +27,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) - - -def _import_configs( - hass: HomeAssistant, - weather_configs: list[ConfigType], - sensor_configs: list[ConfigType], - camera_configs: list[ConfigType], -) -> None: - camera_config = {} - if camera_configs: - camera_config = camera_configs[0] - - for config in sensor_configs: - # Remove weather configurations which share lat/lon with sensor configurations - matching_weather_config = None - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - for weather_config in weather_configs: - weather_latitude = config.get(CONF_LATITUDE, hass.config.latitude) - weather_longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - if latitude == weather_latitude and longitude == weather_longitude: - matching_weather_config = weather_config - break - - if matching_weather_config is not None: - weather_configs.remove(matching_weather_config) - - configs = weather_configs + sensor_configs - - if not configs and camera_configs: - config = { - CONF_LATITUDE: hass.config.latitude, - CONF_LONGITUDE: hass.config.longitude, - } - configs.append(config) - - if configs: - _try_update_unique_id(hass, configs[0], camera_config) - - for config in configs: - data = { - CONF_LATITUDE: config.get(CONF_LATITUDE, hass.config.latitude), - CONF_LONGITUDE: config.get(CONF_LONGITUDE, hass.config.longitude), - CONF_TIMEFRAME: config.get(CONF_TIMEFRAME, DEFAULT_TIMEFRAME), - CONF_COUNTRY: camera_config.get(CONF_COUNTRY, DEFAULT_COUNTRY), - CONF_DELTA: camera_config.get(CONF_DELTA, DEFAULT_DELTA), - CONF_NAME: config.get(CONF_NAME, "Buienradar"), - } - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=data, - ) - ) - - -def _try_update_unique_id( - hass: HomeAssistant, config: ConfigType, camera_config: ConfigType -) -> None: - dimension = camera_config.get(CONF_DIMENSION, DEFAULT_DIMENSION) - country = camera_config.get(CONF_COUNTRY, DEFAULT_COUNTRY) - - registry = entity_registry.async_get(hass) - entity_id = registry.async_get_entity_id("camera", DOMAIN, f"{dimension}_{country}") - - if entity_id is not None: - latitude = config[CONF_LATITUDE] - longitude = config[CONF_LONGITUDE] - - new_unique_id = f"{latitude:2.6f}{longitude:2.6f}" - registry.async_update_entity(entity_id, new_unique_id=new_unique_id) - - -def _filter_domain_configs( - config: ConfigType, domain: str, platform: str -) -> list[ConfigType]: - configs = [] - for entry in config: - if entry.startswith(domain): - configs += [x for x in config[entry] if x["platform"] == platform] - return configs diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 059cd79d522..34f1f173319 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -8,11 +8,10 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -20,7 +19,6 @@ from homeassistant.util import dt as dt_util from .const import ( CONF_COUNTRY, CONF_DELTA, - CONF_DIMENSION, DEFAULT_COUNTRY, DEFAULT_DELTA, DEFAULT_DIMENSION, @@ -34,26 +32,6 @@ DIM_RANGE = vol.All(vol.Coerce(int), vol.Range(min=120, max=700)) # Multiple choice for available Radar Map URL SUPPORTED_COUNTRY_CODES = ["NL", "BE"] -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DIMENSION, default=512): DIM_RANGE, - vol.Optional(CONF_DELTA, default=600.0): cv.positive_float, - vol.Optional(CONF_NAME, default="Buienradar loop"): cv.string, - vol.Optional(CONF_COUNTRY, default="NL"): vol.All( - vol.Coerce(str), vol.In(SUPPORTED_COUNTRY_CODES) - ), - } - ) -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up buienradar camera platform.""" - _LOGGER.warning( - "Platform configuration is deprecated, will be removed in a future release" - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/buienradar/config_flow.py b/homeassistant/components/buienradar/config_flow.py index e773b39027e..445c6cacbc8 100644 --- a/homeassistant/components/buienradar/config_flow.py +++ b/homeassistant/components/buienradar/config_flow.py @@ -1,7 +1,6 @@ """Config flow for buienradar integration.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -24,8 +23,6 @@ from .const import ( SUPPORTED_COUNTRY_CODES, ) -_LOGGER = logging.getLogger(__name__) - class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for buienradar.""" @@ -70,18 +67,6 @@ class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors={}, ) - async def async_step_import(self, import_input: dict[str, Any]) -> FlowResult: - """Import a config entry.""" - latitude = import_input[CONF_LATITUDE] - longitude = import_input[CONF_LONGITUDE] - - await self.async_set_unique_id(f"{latitude}-{longitude}") - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=f"{latitude},{longitude}", data=import_input - ) - class BuienradarOptionFlowHandler(config_entries.OptionsFlow): """Handle options.""" diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py index cc785512f9b..6af579dd74f 100644 --- a/homeassistant/components/buienradar/const.py +++ b/homeassistant/components/buienradar/const.py @@ -7,15 +7,10 @@ DEFAULT_TIMEFRAME = 60 DEFAULT_DIMENSION = 700 DEFAULT_DELTA = 600 -CONF_DIMENSION = "dimension" CONF_DELTA = "delta" CONF_COUNTRY = "country_code" CONF_TIMEFRAME = "timeframe" -"""Range according to the docs""" -CAMERA_DIM_MIN = 120 -CAMERA_DIM_MAX = 700 - SUPPORTED_COUNTRY_CODES = ["NL", "BE"] DEFAULT_COUNTRY = "NL" diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 10507166288..e08b8072a18 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -18,15 +18,13 @@ from buienradar.constants import ( WINDGUST, WINDSPEED, ) -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, CONF_NAME, DEGREE, DEVICE_CLASS_TEMPERATURE, @@ -40,7 +38,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -317,31 +314,6 @@ SENSOR_TYPES = { "symbol_5d": ["Symbol 5d", None, None, None], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_MONITORED_CONDITIONS, default=["symbol", "temperature"] - ): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]), - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - vol.Optional(CONF_TIMEFRAME, default=DEFAULT_TIMEFRAME): vol.All( - vol.Coerce(int), vol.Range(min=5, max=120) - ), - vol.Optional(CONF_NAME, default="br"): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up buienradar sensor platform.""" - _LOGGER.warning( - "Platform configuration is deprecated, will be removed in a future release" - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 0aa57efc5f9..0c5080fc58b 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -11,7 +11,6 @@ from buienradar.constants import ( WINDAZIMUTH, WINDSPEED, ) -import voluptuous as vol from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -35,13 +34,11 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, - PLATFORM_SCHEMA, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback # Reuse data and API logic from the sensor implementation @@ -76,22 +73,6 @@ CONDITION_CLASSES = { ATTR_CONDITION_EXCEPTIONAL: (), } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_FORECAST, default=True): cv.boolean, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up buienradar weather platform.""" - _LOGGER.warning( - "Platform configuration is deprecated, will be removed in a future release" - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index b8abefec70a..828101bf77e 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -66,34 +66,6 @@ async def test_config_flow_already_configured_weather(hass): assert result["reason"] == "already_configured" -async def test_import_camera(hass): - """Test import of camera.""" - with patch( - "homeassistant.components.buienradar.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" - assert result["data"] == { - CONF_LATITUDE: TEST_LATITUDE, - CONF_LONGITUDE: TEST_LONGITUDE, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_options_flow(hass): """Test options flow.""" entry = MockConfigEntry( diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py index 0c25fcc1886..568340a0e09 100644 --- a/tests/components/buienradar/test_init.py +++ b/tests/components/buienradar/test_init.py @@ -1,11 +1,7 @@ """Tests for the buienradar component.""" -from unittest.mock import patch - -from homeassistant import setup from homeassistant.components.buienradar.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers.entity_registry import async_get_registry from tests.common import MockConfigEntry @@ -13,91 +9,6 @@ TEST_LATITUDE = 51.5288504 TEST_LONGITUDE = 5.4002156 -async def test_import_all(hass): - """Test import of all platforms.""" - config = { - "weather 1": [{"platform": "buienradar", "name": "test1"}], - "sensor 1": [{"platform": "buienradar", "timeframe": 30, "name": "test2"}], - "camera 1": [ - { - "platform": "buienradar", - "country_code": "BE", - "delta": 300, - "name": "test3", - } - ], - } - - with patch( - "homeassistant.components.buienradar.async_setup_entry", return_value=True - ): - await setup.async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - conf_entries = hass.config_entries.async_entries(DOMAIN) - - assert len(conf_entries) == 1 - - entry = conf_entries[0] - - assert entry.state is ConfigEntryState.LOADED - assert entry.data == { - "latitude": hass.config.latitude, - "longitude": hass.config.longitude, - "timeframe": 30, - "country_code": "BE", - "delta": 300, - "name": "test2", - } - - -async def test_import_camera(hass): - """Test import of camera platform.""" - entity_registry = await async_get_registry(hass) - entity_registry.async_get_or_create( - domain="camera", - platform="buienradar", - unique_id="512_NL", - original_name="test_name", - ) - await hass.async_block_till_done() - - config = { - "camera 1": [{"platform": "buienradar", "country_code": "NL", "dimension": 512}] - } - - with patch( - "homeassistant.components.buienradar.async_setup_entry", return_value=True - ): - await setup.async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - conf_entries = hass.config_entries.async_entries(DOMAIN) - - assert len(conf_entries) == 1 - - entry = conf_entries[0] - - assert entry.state is ConfigEntryState.LOADED - assert entry.data == { - "latitude": hass.config.latitude, - "longitude": hass.config.longitude, - "timeframe": 60, - "country_code": "NL", - "delta": 600, - "name": "Buienradar", - } - - entity_id = entity_registry.async_get_entity_id( - "camera", - "buienradar", - f"{hass.config.latitude:2.6f}{hass.config.longitude:2.6f}", - ) - assert entity_id - entity = entity_registry.async_get(entity_id) - assert entity.original_name == "test_name" - - async def test_load_unload(aioclient_mock, hass): """Test options flow.""" entry = MockConfigEntry( From 82256b25883b4c0747550c6afa398cb1aeaf84d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jul 2021 09:43:07 +0300 Subject: [PATCH 298/818] Bump actions/stale from 3.0.19 to 4 (#53042) Bumps [actions/stale](https://github.com/actions/stale) from 3.0.19 to 4. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v3.0.19...v4) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d41deb9ec92..4770780341d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v3.0.19 + uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -53,7 +53,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v3.0.19 + uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -78,7 +78,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v3.0.19 + uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" From 5ff9c3e61198746a9999f47c0ad123f29e8885bf Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 03:27:31 -0400 Subject: [PATCH 299/818] Clean up freedompro (#52992) --- .../components/freedompro/binary_sensor.py | 15 ++++---------- homeassistant/components/freedompro/light.py | 11 ++++------ homeassistant/components/freedompro/sensor.py | 20 +++++++------------ homeassistant/components/freedompro/switch.py | 17 +++++++--------- 4 files changed, 22 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index b629f4c0af4..133f64019c2 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -6,9 +6,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorEntity, ) -from homeassistant.const import CONF_API_KEY from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -32,10 +30,9 @@ SUPPORTED_SENSORS = {"smokeSensor", "occupancySensor", "motionSensor", "contactS async def async_setup_entry(hass, entry, async_add_entities): """Set up Freedompro binary_sensor.""" - api_key = entry.data[CONF_API_KEY] coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - Device(hass, api_key, device, coordinator) + Device(device, coordinator) for device in coordinator.data if device["type"] in SUPPORTED_SENSORS ) @@ -44,25 +41,21 @@ async def async_setup_entry(hass, entry, async_add_entities): class Device(CoordinatorEntity, BinarySensorEntity): """Representation of an Freedompro binary_sensor.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__(self, device, coordinator): """Initialize the Freedompro binary_sensor.""" super().__init__(coordinator) - self._hass = hass - self._session = aiohttp_client.async_get_clientsession(self._hass) - self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._type = device["type"] - self._characteristics = device["characteristics"] self._attr_device_info = { "name": self.name, "identifiers": { (DOMAIN, self.unique_id), }, - "model": self._type, + "model": device["type"], "manufacturer": "Freedompro", } - self._attr_device_class = DEVICE_CLASS_MAP[self._type] + self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 4b39fa395d1..0b944a682d4 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -36,27 +36,24 @@ class Device(CoordinatorEntity, LightEntity): def __init__(self, hass, api_key, device, coordinator): """Initialize the Freedompro light.""" super().__init__(coordinator) - self._hass = hass - self._session = aiohttp_client.async_get_clientsession(self._hass) + self._session = aiohttp_client.async_get_clientsession(hass) self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._type = device["type"] - self._characteristics = device["characteristics"] self._attr_device_info = { "name": self.name, "identifiers": { (DOMAIN, self.unique_id), }, - "model": self._type, + "model": device["type"], "manufacturer": "Freedompro", } self._attr_is_on = False self._attr_brightness = 0 color_mode = COLOR_MODE_ONOFF - if "hue" in self._characteristics: + if "hue" in device["characteristics"]: color_mode = COLOR_MODE_HS - elif "brightness" in self._characteristics: + elif "brightness" in device["characteristics"]: color_mode = COLOR_MODE_BRIGHTNESS self._attr_color_mode = color_mode self._attr_supported_color_modes = {color_mode} diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 00dbb30570c..0c12f20849c 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -6,9 +6,8 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import CONF_API_KEY, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -38,10 +37,9 @@ SUPPORTED_SENSORS = {"temperatureSensor", "humiditySensor", "lightSensor"} async def async_setup_entry(hass, entry, async_add_entities): """Set up Freedompro sensor.""" - api_key = entry.data[CONF_API_KEY] coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - Device(hass, api_key, device, coordinator) + Device(device, coordinator) for device in coordinator.data if device["type"] in SUPPORTED_SENSORS ) @@ -50,27 +48,23 @@ async def async_setup_entry(hass, entry, async_add_entities): class Device(CoordinatorEntity, SensorEntity): """Representation of an Freedompro sensor.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__(self, device, coordinator): """Initialize the Freedompro sensor.""" super().__init__(coordinator) - self._hass = hass - self._session = aiohttp_client.async_get_clientsession(self._hass) - self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._type = device["type"] - self._characteristics = device["characteristics"] self._attr_device_info = { "name": self.name, "identifiers": { (DOMAIN, self.unique_id), }, - "model": self._type, + "model": device["type"], "manufacturer": "Freedompro", } - self._attr_device_class = DEVICE_CLASS_MAP[self._type] - self._attr_state_class = STATE_CLASS_MAP[self._type] - self._attr_unit_of_measurement = UNIT_MAP[self._type] + self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] + self._attr_state_class = STATE_CLASS_MAP[device["type"]] + self._attr_unit_of_measurement = UNIT_MAP[device["type"]] self._attr_state = 0 @callback diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index 4e3ffb1a2eb..c4c6b8ec353 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -29,19 +29,16 @@ class Device(CoordinatorEntity, SwitchEntity): def __init__(self, hass, api_key, device, coordinator): """Initialize the Freedompro switch.""" super().__init__(coordinator) - self._hass = hass - self._session = aiohttp_client.async_get_clientsession(self._hass) + self._session = aiohttp_client.async_get_clientsession(hass) self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._type = device["type"] - self._characteristics = device["characteristics"] self._attr_device_info = { - "name": self._attr_name, + "name": self.name, "identifiers": { - (DOMAIN, self._attr_unique_id), + (DOMAIN, self.unique_id), }, - "model": self._type, + "model": device["type"], "manufacturer": "Freedompro", } self._attr_is_on = False @@ -53,7 +50,7 @@ class Device(CoordinatorEntity, SwitchEntity): ( device for device in self.coordinator.data - if device["uid"] == self._attr_unique_id + if device["uid"] == self.unique_id ), None, ) @@ -75,7 +72,7 @@ class Device(CoordinatorEntity, SwitchEntity): await put_state( self._session, self._api_key, - self._attr_unique_id, + self.unique_id, payload, ) await self.coordinator.async_request_refresh() @@ -87,7 +84,7 @@ class Device(CoordinatorEntity, SwitchEntity): await put_state( self._session, self._api_key, - self._attr_unique_id, + self.unique_id, payload, ) await self.coordinator.async_request_refresh() From f07d64c813b78106dd654e4142707942373a3d5b Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 15 Jul 2021 09:31:17 +0200 Subject: [PATCH 300/818] Another SIA fix for timestamp not present. (#53045) --- homeassistant/components/sia/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 6b87c9cb1fc..9150099656c 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -25,7 +25,9 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: ATTR_CODE: event.code, ATTR_MESSAGE: event.message, ATTR_ID: event.id, - ATTR_TIMESTAMP: event.timestamp.isoformat(), + ATTR_TIMESTAMP: event.timestamp.isoformat() + if event.timestamp + else utcnow().isoformat(), } From a057fd93bbca2deb5810e8a53c8848a557a5959a Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Thu, 15 Jul 2021 09:31:50 +0200 Subject: [PATCH 301/818] Add lock support Freedompro (#52725) * change _attr_unique_id to unique_id and resolve conflict * add test state updates from the API * optimizer code test * fix test * fix comments and add test device registry --- .../components/freedompro/__init__.py | 2 +- homeassistant/components/freedompro/lock.py | 95 ++++++++++++++ tests/components/freedompro/test_lock.py | 120 ++++++++++++++++++ 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/freedompro/lock.py create mode 100644 tests/components/freedompro/test_lock.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 6b377f51560..311f1794866 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "lock", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py new file mode 100644 index 00000000000..f3a689016f6 --- /dev/null +++ b/homeassistant/components/freedompro/lock.py @@ -0,0 +1,95 @@ +"""Support for Freedompro lock.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.lock import LockEntity +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro lock.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] == "lock" + ) + + +class Device(CoordinatorEntity, LockEntity): + """Representation of an Freedompro lock.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro lock.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": self._type, + "manufacturer": "Freedompro", + } + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "lock" in state: + if state["lock"] == 1: + self._attr_is_locked = True + else: + self._attr_is_locked = False + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_lock(self, **kwargs): + """Async function to lock the lock.""" + payload = {"lock": 1} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_unlock(self, **kwargs): + """Async function to unlock the lock.""" + payload = {"lock": 0} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py new file mode 100644 index 00000000000..5c30909e081 --- /dev/null +++ b/tests/components/freedompro/test_lock.py @@ -0,0 +1,120 @@ +"""Tests for the Freedompro lock.""" +from datetime import timedelta +from unittest.mock import ANY, patch + +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + SERVICE_LOCK, + SERVICE_UNLOCK, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + +uid = "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0" + + +async def test_lock_get_state(hass, init_integration): + """Test states of the lock.""" + init_integration + registry = er.async_get(hass) + registry_device = dr.async_get(hass) + + device = registry_device.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == "lock" + assert device.model == "lock" + + entity_id = "lock.lock" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNLOCKED + assert state.attributes.get("friendly_name") == "lock" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + state_response["state"]["lock"] = 1 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "lock" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_LOCKED + + +async def test_lock_set_unlock(hass, init_integration): + """Test set on of the lock.""" + init_integration + registry = er.async_get(hass) + + entity_id = "lock.lock" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_LOCKED + assert state.attributes.get("friendly_name") == "lock" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.lock.put_state") as mock_put_state: + assert await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"lock": 0}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_LOCKED + + +async def test_lock_set_lock(hass, init_integration): + """Test set on of the lock.""" + init_integration + registry = er.async_get(hass) + + entity_id = "lock.lock" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_LOCKED + assert state.attributes.get("friendly_name") == "lock" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.lock.put_state") as mock_put_state: + assert await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"lock": 1}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_LOCKED From eee3aa3b6f7340705956f0e187d16e4950254c09 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 04:00:47 -0400 Subject: [PATCH 302/818] Use entity class attributes for bme680 (#53037) * Use entity class attributes for bme680 * fix --- homeassistant/components/bme680/sensor.py | 36 +++++++---------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 6e4e2de79b9..527a971b237 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -323,45 +323,29 @@ class BME680Sensor(SensorEntity): def __init__(self, bme680_client, sensor_type, temp_unit, name): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] + self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self.bme680_client = bme680_client self.temp_unit = temp_unit self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return self._unit_of_measurement - async def async_update(self): """Get the latest data from the BME680 and update the states.""" await self.hass.async_add_executor_job(self.bme680_client.update) if self.type == SENSOR_TEMP: - temperature = round(self.bme680_client.sensor_data.temperature, 1) + self._attr_state = round(self.bme680_client.sensor_data.temperature, 1) if self.temp_unit == TEMP_FAHRENHEIT: - temperature = round(celsius_to_fahrenheit(temperature), 1) - self._state = temperature + self._attr_state = round(celsius_to_fahrenheit(self.state), 1) elif self.type == SENSOR_HUMID: - self._state = round(self.bme680_client.sensor_data.humidity, 1) + self._attr_state = round(self.bme680_client.sensor_data.humidity, 1) elif self.type == SENSOR_PRESS: - self._state = round(self.bme680_client.sensor_data.pressure, 1) + self._attr_state = round(self.bme680_client.sensor_data.pressure, 1) elif self.type == SENSOR_GAS: - self._state = int(round(self.bme680_client.sensor_data.gas_resistance, 0)) + self._attr_state = int( + round(self.bme680_client.sensor_data.gas_resistance, 0) + ) elif self.type == SENSOR_AQ: aq_score = self.bme680_client.sensor_data.air_quality if aq_score is not None: - self._state = round(aq_score, 1) + self._attr_state = round(aq_score, 1) From 8ce4d647c37efd2e3a5b37cb986427e65edba66c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 04:19:18 -0400 Subject: [PATCH 303/818] Use entity class attributes for arcam_fmj (#52675) * Use entity class attributes for arcam_fmj * fix --- .../components/arcam_fmj/media_player.py | 45 +++++-------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 8a119d020fe..89f0cc3b112 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -63,6 +63,8 @@ async def async_setup_entry( class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" + _attr_should_poll = False + def __init__( self, device_name, @@ -72,9 +74,9 @@ class ArcamFmj(MediaPlayerEntity): """Initialize device.""" self._state = state self._device_name = device_name - self._name = f"{device_name} - Zone: {state.zn}" + self._attr_name = f"{device_name} - Zone: {state.zn}" self._uuid = uuid - self._support = ( + self._attr_supported_features = ( SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | SUPPORT_BROWSE_MEDIA @@ -85,7 +87,9 @@ class ArcamFmj(MediaPlayerEntity): | SUPPORT_TURN_ON ) if state.zn == 1: - self._support |= SUPPORT_SELECT_SOUND_MODE + self._attr_supported_features |= SUPPORT_SELECT_SOUND_MODE + self._attr_unique_id = f"{uuid}-{state.zn}" + self._attr_entity_registry_enabled_default = state.zn == 1 def _get_2ch(self): """Return if source is 2 channel or not.""" @@ -101,14 +105,11 @@ class ArcamFmj(MediaPlayerEntity): ) @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._state.zn == 1 - - @property - def unique_id(self): - """Return unique identifier if known.""" - return f"{self._uuid}-{self._state.zn}" + def state(self): + """Return the state of the device.""" + if self._state.get_power(): + return STATE_ON + return STATE_OFF @property def device_info(self): @@ -123,28 +124,6 @@ class ArcamFmj(MediaPlayerEntity): "manufacturer": "Arcam", } - @property - def should_poll(self) -> bool: - """No need to poll.""" - return False - - @property - def name(self): - """Return the name of the controlled device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - if self._state.get_power(): - return STATE_ON - return STATE_OFF - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return self._support - async def async_added_to_hass(self): """Once registered, add listener for events.""" await self._state.start() From c9eab101349e4b91650641d4d825ea65e74eb8a4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 15 Jul 2021 11:12:23 +0200 Subject: [PATCH 304/818] Add MQTT humidifier platform integration (#52828) * New mqtt humidifier platform * Add humidifier platform * Leave out humidity step * Use humidity in constant for payload reset * change TARGET_HUMIDITY_RESET payload name * _attr_max_humidity not assigned correctly * _target_humidity_range has a zero base * align CONF_TARGET_HUMIDITY_MIN and MAX with model * shorter topics for humidity_range * Converts float to int from template * new humidifier abbreviations * Add common module to support tests * Add tests * Addtional testing * Always require target_humidity_command_topic * Typo * use available_modes to align entity model * use avail_modes not modes to avoid conflict * typo target_humidity_value_template * Allign modes and templates with climate platform * mode_state_template * target_humidity_state_template * Typo in platform name * Remove humidity_range feature and common lib * Update homeassistant/components/mqtt/humidifier.py Use vol.In, not regex Co-authored-by: Erik Montnemery * black * Update homeassistant/components/mqtt/humidifier.py Co-authored-by: Erik Montnemery * Use round to convert float to target humidity Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + .../components/mqtt/abbreviations.py | 11 +- homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/humidifier.py | 456 +++++++ tests/components/mqtt/test_humidifier.py | 1052 +++++++++++++++++ 5 files changed, 1520 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/mqtt/humidifier.py create mode 100644 tests/components/mqtt/test_humidifier.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e524502dd8d..e95729602cc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -126,6 +126,7 @@ PLATFORMS = [ "climate", "cover", "fan", + "humidifier", "light", "lock", "number", diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index a2bd7fc6b36..6bb7a92e8af 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -74,6 +74,10 @@ ABBREVIATIONS = { "hs_val_tpl": "hs_value_template", "ic": "icon", "init": "initial", + "hum_cmd_t": "target_humidity_command_topic", + "hum_cmd_tpl": "target_humidity_command_template", + "hum_stat_t": "target_humidity_state_topic", + "hum_state_tpl": "target_humidity_state_template", "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", @@ -81,14 +85,17 @@ ABBREVIATIONS = { "lrst_val_tpl": "last_reset_value_template", "max": "max", "min": "min", + "max_hum": "max_humidity", + "min_hum": "min_humidity", "max_mirs": "max_mireds", "min_mirs": "min_mireds", "max_temp": "max_temp", "min_temp": "min_temp", "mode_cmd_tpl": "mode_command_template", "mode_cmd_t": "mode_command_topic", - "mode_stat_tpl": "mode_state_template", "mode_stat_t": "mode_state_topic", + "mode_stat_tpl": "mode_state_template", + "modes": "modes", "name": "name", "off_dly": "off_delay", "on_cmd_type": "on_command_type", @@ -126,6 +133,8 @@ ABBREVIATIONS = { "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_paus": "payload_pause", + "pl_rst_hum": "payload_reset_humidity", + "pl_rst_mode": "payload_reset_mode", "pl_rst_pct": "payload_reset_percentage", "pl_rst_pr_mode": "payload_reset_preset_mode", "pl_stop": "payload_stop", diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 84d85fba79c..0659baa9144 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -41,6 +41,7 @@ SUPPORTED_COMPONENTS = [ "device_automation", "device_tracker", "fan", + "humidifier", "light", "lock", "number", diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py new file mode 100644 index 00000000000..4e7e9ee5879 --- /dev/null +++ b/homeassistant/components/mqtt/humidifier.py @@ -0,0 +1,456 @@ +"""Support for MQTT humidifiers.""" +import functools +import logging + +import voluptuous as vol + +from homeassistant.components import humidifier +from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + ATTR_MODE, + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + SUPPORT_MODES, + HumidifierEntity, +) +from homeassistant.const import ( + CONF_NAME, + CONF_OPTIMISTIC, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, + CONF_STATE, +) +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType + +from . import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, + subscription, +) +from .. import mqtt +from .debug_info import log_messages +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper + +CONF_AVAILABLE_MODES_LIST = "modes" +CONF_COMMAND_TEMPLATE = "command_template" +CONF_DEVICE_CLASS = "device_class" +CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" +CONF_MODE_COMMAND_TOPIC = "mode_command_topic" +CONF_MODE_STATE_TOPIC = "mode_state_topic" +CONF_MODE_STATE_TEMPLATE = "mode_state_template" +CONF_PAYLOAD_RESET_MODE = "payload_reset_mode" +CONF_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidity" +CONF_STATE_VALUE_TEMPLATE = "state_value_template" +CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" +CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_TARGET_HUMIDITY_MIN = "min_humidity" +CONF_TARGET_HUMIDITY_MAX = "max_humidity" +CONF_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" +CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" + +DEFAULT_NAME = "MQTT Humidifier" +DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_PAYLOAD_RESET = "None" + +MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED = frozenset( + { + humidifier.ATTR_HUMIDITY, + humidifier.ATTR_MAX_HUMIDITY, + humidifier.ATTR_MIN_HUMIDITY, + humidifier.ATTR_MODE, + humidifier.ATTR_AVAILABLE_MODES, + } +) + +_LOGGER = logging.getLogger(__name__) + + +def valid_mode_configuration(config): + """Validate that the mode reset payload is not one of the available modes.""" + if config.get(CONF_PAYLOAD_RESET_MODE) in config.get(CONF_AVAILABLE_MODES_LIST): + raise ValueError("modes must not contain payload_reset_mode") + return config + + +def valid_humidity_range_configuration(config): + """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + if config.get(CONF_TARGET_HUMIDITY_MIN) >= config.get(CONF_TARGET_HUMIDITY_MAX): + raise ValueError("target_humidity_max must be > target_humidity_min") + if config.get(CONF_TARGET_HUMIDITY_MAX) > 100: + raise ValueError("max_humidity must be <= 100") + + return config + + +PLATFORM_SCHEMA = vol.All( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + # CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together + vol.Inclusive( + CONF_AVAILABLE_MODES_LIST, "available_modes", default=[] + ): cv.ensure_list, + vol.Inclusive( + CONF_MODE_COMMAND_TOPIC, "available_modes" + ): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_HUMIDIFIER): vol.In( + [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] + ), + vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, + vol.Optional( + CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY + ): cv.positive_int, + vol.Optional( + CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY + ): cv.positive_int, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional( + CONF_PAYLOAD_RESET_MODE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + } + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + valid_humidity_range_configuration, + valid_mode_configuration, +) + + +async def async_setup_platform( + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT humidifier through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_entity(hass, async_add_entities, config) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT humidifier dynamically through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, humidifier.DOMAIN, setup, PLATFORM_SCHEMA) + + +async def _async_setup_entity( + hass, async_add_entities, config, config_entry=None, discovery_data=None +): + """Set up the MQTT humidifier.""" + async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) + + +class MqttHumidifier(MqttEntity, HumidifierEntity): + """A MQTT humidifier component.""" + + _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED + + def __init__(self, hass, config, config_entry, discovery_data): + """Initialize the MQTT humidifier.""" + self._state = False + self._target_humidity = None + self._mode = None + self._supported_features = 0 + + self._topic = None + self._payload = None + self._value_templates = None + self._command_templates = None + self._optimistic = None + self._optimistic_target_humidity = None + self._optimistic_mode = None + + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_min_humidity = config.get(CONF_TARGET_HUMIDITY_MIN) + self._attr_max_humidity = config.get(CONF_TARGET_HUMIDITY_MAX) + + self._topic = { + key: config.get(key) + for key in ( + CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, + CONF_TARGET_HUMIDITY_STATE_TOPIC, + CONF_TARGET_HUMIDITY_COMMAND_TOPIC, + CONF_MODE_STATE_TOPIC, + CONF_MODE_COMMAND_TOPIC, + ) + } + self._value_templates = { + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), + } + self._command_templates = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), + } + self._payload = { + "STATE_ON": config[CONF_PAYLOAD_ON], + "STATE_OFF": config[CONF_PAYLOAD_OFF], + "HUMIDITY_RESET": config[CONF_PAYLOAD_RESET_HUMIDITY], + "MODE_RESET": config[CONF_PAYLOAD_RESET_MODE], + } + if CONF_MODE_COMMAND_TOPIC in config and CONF_AVAILABLE_MODES_LIST in config: + self._available_modes = config[CONF_AVAILABLE_MODES_LIST] + else: + self._available_modes = [] + if self._available_modes: + self._attr_supported_features = SUPPORT_MODES + else: + self._attr_supported_features = 0 + + optimistic = config[CONF_OPTIMISTIC] + self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None + self._optimistic_target_humidity = ( + optimistic or self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is None + ) + self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None + + for tpl_dict in [self._command_templates, self._value_templates]: + for key, tpl in tpl_dict.items(): + if tpl is None: + tpl_dict[key] = lambda value: value + else: + tpl.hass = self.hass + tpl_dict[key] = tpl.async_render_with_possible_json_value + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + topics = {} + + @callback + @log_messages(self.hass, self.entity_id) + def state_received(msg): + """Handle new received MQTT message.""" + payload = self._value_templates[CONF_STATE](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) + return + if payload == self._payload["STATE_ON"]: + self._state = True + elif payload == self._payload["STATE_OFF"]: + self._state = False + self.async_write_ha_state() + + if self._topic[CONF_STATE_TOPIC] is not None: + topics[CONF_STATE_TOPIC] = { + "topic": self._topic[CONF_STATE_TOPIC], + "msg_callback": state_received, + "qos": self._config[CONF_QOS], + } + + @callback + @log_messages(self.hass, self.entity_id) + def target_humidity_received(msg): + """Handle new received MQTT message for the target humidity.""" + rendered_target_humidity_payload = self._value_templates[ATTR_HUMIDITY]( + msg.payload + ) + if not rendered_target_humidity_payload: + _LOGGER.debug("Ignoring empty target humidity from '%s'", msg.topic) + return + if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: + self._target_humidity = None + self.async_write_ha_state() + return + try: + target_humidity = round(float(rendered_target_humidity_payload)) + except ValueError: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid target humidity", + msg.payload, + msg.topic, + rendered_target_humidity_payload, + ) + return + if ( + target_humidity < self._attr_min_humidity + or target_humidity > self._attr_max_humidity + ): + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid target humidity", + msg.payload, + msg.topic, + rendered_target_humidity_payload, + ) + return + self._target_humidity = target_humidity + self.async_write_ha_state() + + if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: + topics[CONF_TARGET_HUMIDITY_STATE_TOPIC] = { + "topic": self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC], + "msg_callback": target_humidity_received, + "qos": self._config[CONF_QOS], + } + self._target_humidity = None + + @callback + @log_messages(self.hass, self.entity_id) + def mode_received(msg): + """Handle new received MQTT message for mode.""" + mode = self._value_templates[ATTR_MODE](msg.payload) + if mode == self._payload["MODE_RESET"]: + self._mode = None + self.async_write_ha_state() + return + if not mode: + _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) + return + if mode not in self.available_modes: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid mode", + msg.payload, + msg.topic, + mode, + ) + return + + self._mode = mode + self.async_write_ha_state() + + if self._topic[CONF_MODE_STATE_TOPIC] is not None: + topics[CONF_MODE_STATE_TOPIC] = { + "topic": self._topic[CONF_MODE_STATE_TOPIC], + "msg_callback": mode_received, + "qos": self._config[CONF_QOS], + } + self._mode = None + + self._sub_state = await subscription.async_subscribe_topics( + self.hass, self._sub_state, topics + ) + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def available_modes(self) -> list: + """Get the list of available modes.""" + return self._available_modes + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def target_humidity(self): + """Return the current target humidity.""" + return self._target_humidity + + @property + def mode(self): + """Return the current mode.""" + return self._mode + + async def async_turn_on( + self, + **kwargs, + ) -> None: + """Turn on the entity. + + This method is a coroutine. + """ + mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) + mqtt.async_publish( + self.hass, + self._topic[CONF_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + if self._optimistic: + self._state = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn off the entity. + + This method is a coroutine. + """ + mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) + mqtt.async_publish( + self.hass, + self._topic[CONF_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + if self._optimistic: + self._state = False + self.async_write_ha_state() + + async def async_set_humidity(self, humidity: int) -> None: + """Set the target humidity of the humidifier. + + This method is a coroutine. + """ + mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity) + mqtt.async_publish( + self.hass, + self._topic[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + if self._optimistic_target_humidity: + self._target_humidity = humidity + self.async_write_ha_state() + + async def async_set_mode(self, mode: str) -> None: + """Set the mode of the fan. + + This method is a coroutine. + """ + if mode not in self.available_modes: + _LOGGER.warning("'%s'is not a valid mode", mode) + return + + mqtt_payload = self._command_templates[ATTR_MODE](mode) + + mqtt.async_publish( + self.hass, + self._topic[CONF_MODE_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + if self._optimistic_mode: + self._mode = mode + self.async_write_ha_state() diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py new file mode 100644 index 00000000000..4ae834be5da --- /dev/null +++ b/tests/components/mqtt/test_humidifier.py @@ -0,0 +1,1052 @@ +"""Test MQTT humidifiers.""" +from unittest.mock import patch + +import pytest +from voluptuous.error import MultipleInvalid + +from homeassistant.components import humidifier +from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + ATTR_MODE, + DOMAIN, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, +) +from homeassistant.components.mqtt.humidifier import MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message + +DEFAULT_CONFIG = { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + } +} + + +async def async_turn_on( + hass, + entity_id=ENTITY_MATCH_ALL, +) -> None: + """Turn all or specified humidifier on.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: + """Turn all or specified humidier off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + +async def async_set_mode(hass, entity_id=ENTITY_MATCH_ALL, mode: str = None) -> None: + """Set mode for all or specified humidifier.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_MODE, mode)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_MODE, data, blocking=True) + + +async def async_set_humidity( + hass, entity_id=ENTITY_MATCH_ALL, humidity: int = None +) -> None: + """Set target humidity for all or specified humidifier.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_HUMIDITY, humidity)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) + + +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): + """Test if command fails with command topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, + ) + await hass.async_block_till_done() + assert hass.states.get("humidifier.test") is None + + +async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "comfort", + "home", + "eco", + "sleep", + "baby", + ], + "payload_reset_humidity": "rEset_humidity", + "payload_reset_mode": "rEset_mode", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "StAtE_On") + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "state-topic", "StAtE_OfF") + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "humidity-state-topic", "0") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + + async_fire_mqtt_message(hass, "humidity-state-topic", "25") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 25 + + async_fire_mqtt_message(hass, "humidity-state-topic", "50") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 50 + + async_fire_mqtt_message(hass, "humidity-state-topic", "100") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + + async_fire_mqtt_message(hass, "humidity-state-topic", "101") + assert "not a valid target humidity" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "humidity-state-topic", "invalid") + assert "not a valid target humidity" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", "auto") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message(hass, "mode-state-topic", "eco") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + + async_fire_mqtt_message(hass, "mode-state-topic", "baby") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "baby" + + async_fire_mqtt_message(hass, "mode-state-topic", "ModeUnknown") + assert "not a valid mode" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", "rEset_mode") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) is None + + async_fire_mqtt_message(hass, "humidity-state-topic", "rEset_humidity") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + + +async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.val }}", + "target_humidity_state_template": "{{ value_json.val }}", + "mode_state_template": "{{ value_json.val }}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", '{"val":"ON"}') + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "state-topic", '{"val":"OFF"}') + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"val": 1}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 1 + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"val": 100}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"val": "None"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"otherval": 100}') + assert "Ignoring empty target humidity from" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "low"}') + assert "not a valid mode" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "auto"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "eco"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "baby"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "baby" + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "None"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) is None + + async_fire_mqtt_message(hass, "mode-state-topic", '{"otherval": 100}') + assert "Ignoring empty mode from" in caplog.text + caplog.clear() + + +async def test_controlling_state_via_topic_and_json_message_shared_topic( + hass, mqtt_mock, caplog +): + """Test the controlling state via topic and JSON message using a shared topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "shared-state-topic", + "target_humidity_command_topic": "percentage-command-topic", + "mode_state_topic": "shared-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.state }}", + "target_humidity_state_template": "{{ value_json.humidity }}", + "mode_state_template": "{{ value_json.mode }}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","mode":"eco","humidity": 50}', + ) + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 50 + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","mode":"auto","humidity": 10}', + ) + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 10 + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"OFF","mode":"auto","humidity": 0}', + ) + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"humidity": 100}', + ) + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + assert "Ignoring empty mode from" in caplog.text + assert "Ignoring empty state from" in caplog.text + caplog.clear() + + +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): + """Test optimistic mode without state topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "eco", + "auto", + "baby", + ], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "StAtE_On", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_off(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "StAtE_OfF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", -1) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", 101) + + await async_set_humidity(hass, "humidifier.test", 100) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 0) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + await async_set_mode(hass, "humidifier.test", "auto") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "auto", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "eco") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): + """Testing command templates with optimistic mode without state topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "command_template": "state: {{ value }}", + "target_humidity_command_topic": "humidity-command-topic", + "target_humidity_command_template": "humidity: {{ value }}", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "mode: {{ value }}", + "modes": [ + "auto", + "eco", + "sleep", + ], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "state: ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_off(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "state: OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", -1) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", 101) + + await async_set_humidity(hass, "humidifier.test", 100) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "humidity: 100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 0) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "humidity: 0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + await async_set_mode(hass, "humidifier.test", "eco") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "mode: eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "auto") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "mode: auto", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): + """Test optimistic mode with state topic and turn on attributes.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "mode_state_topic": "mode-state-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "optimistic": True, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_off(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 33) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "33", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 50) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "50", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 100) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 0) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", 101) + + await async_set_mode(hass, "humidifier.test", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + await async_set_mode(hass, "humidifier.test", "eco") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "baby") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "baby", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "freaking-high") + assert "not a valid mode" in caplog.text + caplog.clear() + + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_attributes(hass, mqtt_mock, caplog): + """Test attributes.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "mode_command_topic": "mode-command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "modes": [ + "eco", + "baby", + ], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(humidifier.ATTR_AVAILABLE_MODES) == [ + "eco", + "baby", + ] + assert state.attributes.get(humidifier.ATTR_MIN_HUMIDITY) == 0 + assert state.attributes.get(humidifier.ATTR_MAX_HUMIDITY) == 100 + + await async_turn_on(hass, "humidifier.test") + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + assert state.attributes.get(humidifier.ATTR_MODE) is None + + await async_turn_off(hass, "humidifier.test") + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + assert state.attributes.get(humidifier.ATTR_MODE) is None + + +async def test_invalid_configurations(hass, mqtt_mock, caplog): + """Test invalid configurations.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: [ + { + "platform": "mqtt", + "name": "test_valid_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + { + "platform": "mqtt", + "name": "test_valid_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "humidifier", + }, + { + "platform": "mqtt", + "name": "test_valid_3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "dehumidifier", + }, + { + "platform": "mqtt", + "name": "test_invalid_device_class", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "notsupporedSpeci@l", + }, + { + "platform": "mqtt", + "name": "test_mode_command_without_modes", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + }, + { + "platform": "mqtt", + "name": "test_invalid_humidity_min_max_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "min_humidity": 0, + "max_humidity": 101, + }, + { + "platform": "mqtt", + "name": "test_invalid_humidity_min_max_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "max_humidity": 20, + "min_humidity": 40, + }, + { + "platform": "mqtt", + "name": "test_invalid_mode_is_reset", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "None"], + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get("humidifier.test_valid_1") is not None + assert hass.states.get("humidifier.test_valid_2") is not None + assert hass.states.get("humidifier.test_valid_3") is not None + assert hass.states.get("humidifier.test_invalid_device_class") is None + assert hass.states.get("humidifier.test_mode_command_without_modes") is None + assert "not all values in the same group of inclusion" in caplog.text + caplog.clear() + + assert hass.states.get("humidifier.test_invalid_humidity_min_max_1") is None + assert hass.states.get("humidifier.test_invalid_humidity_min_max_2") is None + assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None + + +async def test_supported_features(hass, mqtt_mock): + """Test supported features.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: [ + { + "platform": "mqtt", + "name": "test1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + { + "platform": "mqtt", + "name": "test2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + { + "platform": "mqtt", + "name": "test3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + { + "platform": "mqtt", + "name": "test4", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + { + "platform": "mqtt", + "name": "test5", + "command_topic": "command-topic", + }, + { + "platform": "mqtt", + "name": "test6", + "target_humidity_command_topic": "humidity-command-topic", + }, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test1") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + + state = hass.states.get("humidifier.test2") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES + + state = hass.states.get("humidifier.test3") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + + state = hass.states.get("humidifier.test4") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES + + state = hass.states.get("humidifier.test5") + assert state is None + + state = hass.states.get("humidifier.test6") + assert state is None + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + ) + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock, + humidifier.DOMAIN, + DEFAULT_CONFIG, + MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique_id option only creates one fan per id.""" + config = { + humidifier.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, humidifier.DOMAIN, config) + + +async def test_discovery_removal_humidifier(hass, mqtt_mock, caplog): + """Test removal of discovered humidifier.""" + data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + await help_test_discovery_removal(hass, mqtt_mock, caplog, humidifier.DOMAIN, data) + + +async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): + """Test update of discovered humidifier.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + await help_test_discovery_update( + hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + ) + + +async def test_discovery_update_unchanged_humidifier(hass, mqtt_mock, caplog): + """Test update of discovered humidifier.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + with patch( + "homeassistant.components.mqtt.fan.MqttFan.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + await help_test_discovery_broken( + hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT fan device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT fan device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) From 6c476b5c1e47a6d14eb11df1adb5e7d1120f0485 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 06:35:47 -0400 Subject: [PATCH 305/818] Use entity class attributes for Bmp280 (#53036) --- homeassistant/components/bmp280/sensor.py | 48 +++++------------------ 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index ac607578299..7bf355bb736 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -77,36 +77,8 @@ class Bmp280Sensor(SensorEntity): ) -> None: """Initialize the sensor.""" self._bmp280 = bmp280 - self._name = name - self._unit_of_measurement = unit_of_measurement - self._device_class = device_class - self._state = None - self._errored = False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - @property - def available(self) -> bool: - """Return if the device is currently available.""" - return not self._errored + self._attr_name = name + self._attr_unit_of_measurement = unit_of_measurement class Bmp280TemperatureSensor(Bmp280Sensor): @@ -122,16 +94,16 @@ class Bmp280TemperatureSensor(Bmp280Sensor): def update(self): """Fetch new state data for the sensor.""" try: - self._state = round(self._bmp280.temperature, 1) - if self._errored: + self._attr_state = round(self._bmp280.temperature, 1) + if not self.available: _LOGGER.warning("Communication restored with temperature sensor") - self._errored = False + self._attr_available = True except OSError: # this is thrown when a working sensor is unplugged between two updates _LOGGER.warning( "Unable to read temperature data due to a communication problem" ) - self._errored = True + self._attr_available = False class Bmp280PressureSensor(Bmp280Sensor): @@ -147,13 +119,13 @@ class Bmp280PressureSensor(Bmp280Sensor): def update(self): """Fetch new state data for the sensor.""" try: - self._state = round(self._bmp280.pressure) - if self._errored: + self._attr_state = round(self._bmp280.pressure) + if not self.available: _LOGGER.warning("Communication restored with pressure sensor") - self._errored = False + self._attr_available = True except OSError: # this is thrown when a working sensor is unplugged between two updates _LOGGER.warning( "Unable to read pressure data due to a communication problem" ) - self._errored = True + self._attr_available = False From 519efd2723f81b7a18df7f1dd7f7a9a24a897df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 15 Jul 2021 14:16:30 +0200 Subject: [PATCH 306/818] Handle missing peername (#53052) * Handle missing peername * Add test --- homeassistant/components/hassio/ingress.py | 8 +++++-- tests/components/hassio/test_ingress.py | 28 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index b0c6e9d1dbe..e58c2d790f2 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -8,7 +8,7 @@ import os import aiohttp from aiohttp import ClientTimeout, hdrs, web -from aiohttp.web_exceptions import HTTPBadGateway +from aiohttp.web_exceptions import HTTPBadGateway, HTTPBadRequest from multidict import CIMultiDict from homeassistant.components.http import HomeAssistantView @@ -185,7 +185,11 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) - connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) + if (peername := request.transport.get_extra_info("peername")) is None: + _LOGGER.error("Can't set forward_for header, missing peername") + raise HTTPBadRequest() + + connected_ip = ip_address(peername[0]) if forward_for: forward_for = f"{forward_for}, {connected_ip!s}" else: diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index e75de8741bf..8f7d97213e0 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -1,5 +1,7 @@ """The tests for the hassio component.""" +from unittest.mock import MagicMock, patch + from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO import pytest @@ -275,3 +277,29 @@ async def test_ingress_websocket(hassio_client, build_type, aioclient_mock): assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +async def test_ingress_missing_peername(hassio_client, aioclient_mock, caplog): + """Test hadnling of missing peername.""" + aioclient_mock.get( + "http://127.0.0.1/ingress/lorem/ipsum", + text="test", + ) + + def get_extra_info(_): + return None + + with patch( + "aiohttp.web_request.BaseRequest.transport", + return_value=MagicMock(), + ) as transport_mock: + transport_mock.get_extra_info = get_extra_info + resp = await hassio_client.get( + "/api/hassio_ingress/lorem/ipsum", + headers={"X-Test-Header": "beer"}, + ) + + assert "Can't set forward_for header, missing peername" in caplog.text + + # Check we got right response + assert resp.status == 400 From 6fe38eadf2c8307cdd17616ea10a163f4b767a9f Mon Sep 17 00:00:00 2001 From: da-anda Date: Thu, 15 Jul 2021 14:41:04 +0200 Subject: [PATCH 307/818] Fix knx expose feature not correctly falling back to default value (#53046) --- homeassistant/components/knx/expose.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 5b57e2b0b4c..5b92f9f1f6a 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -100,10 +100,8 @@ class KNXExposeSensor: def _init_expose_state(self) -> None: """Initialize state of the exposure.""" init_state = self.hass.states.get(self.entity_id) - init_value = self._get_expose_value(init_state) - self.device.sensor_value.value = ( - init_value if init_value is not None else self.expose_default - ) + state_value = self._get_expose_value(init_state) + self.device.sensor_value.value = state_value @callback def shutdown(self) -> None: @@ -116,12 +114,13 @@ class KNXExposeSensor: def _get_expose_value(self, state: State | None) -> StateType: """Extract value from state.""" if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): - return None - value = ( - state.state - if self.expose_attribute is None - else state.attributes.get(self.expose_attribute) - ) + value = self.expose_default + else: + value = ( + state.state + if self.expose_attribute is None + else state.attributes.get(self.expose_attribute, self.expose_default) + ) if self.type == "binary": if value in (1, STATE_ON, "True"): return True @@ -150,9 +149,7 @@ class KNXExposeSensor: async def _async_set_knx_value(self, value: StateType) -> None: """Set new value on xknx ExposeSensor.""" if value is None: - if self.expose_default is None: - return - value = self.expose_default + return await self.device.set(value) From 19b4d2e4d29d5f329a8a0c3e4cfa1af0cf2a70b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Jul 2021 02:43:35 -1000 Subject: [PATCH 308/818] Add OUIs for legacy samsungtv (#52928) --- .../components/samsungtv/manifest.json | 6 +- homeassistant/generated/dhcp.py | 16 ++++ .../components/samsungtv/test_config_flow.py | 84 +++++++++++++++++-- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 4ffe940f946..133baccf4fb 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -18,7 +18,11 @@ "dhcp": [ { "hostname": "tizen*" - } + }, + {"macaddress": "8CC8CD*"}, + {"macaddress": "606BBD*"}, + {"macaddress": "F47B5E*"}, + {"macaddress": "4844F7*"} ], "codeowners": [ "@escoand", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 82b09e5f7ef..dbdaaf6da5e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -175,6 +175,22 @@ DHCP = [ "domain": "samsungtv", "hostname": "tizen*" }, + { + "domain": "samsungtv", + "macaddress": "8CC8CD*" + }, + { + "domain": "samsungtv", + "macaddress": "606BBD*" + }, + { + "domain": "samsungtv", + "macaddress": "F47B5E*" + }, + { + "domain": "samsungtv", + "macaddress": "4844F7*" + }, { "domain": "screenlogic", "hostname": "pentair: *", diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 502b4f0ced8..4fe8ddc2b5e 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -3,7 +3,7 @@ import socket from unittest.mock import Mock, PropertyMock, call, patch from samsungctl.exceptions import AccessDenied, UnhandledResponse -from samsungtvws.exceptions import ConnectionFailure +from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException, WebSocketProtocolException from homeassistant import config_entries @@ -20,7 +20,6 @@ from homeassistant.components.samsungtv.const import ( RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, - RESULT_SUCCESS, RESULT_UNKNOWN_HOST, TIMEOUT_REQUEST, TIMEOUT_WEBSOCKET, @@ -87,6 +86,7 @@ MOCK_SSDP_DATA_WRONGMODEL = { ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", } MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"} +EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = { CONF_HOST: "fake_host", CONF_PORT: 1234, @@ -100,7 +100,13 @@ MOCK_ZEROCONF_DATA = { MOCK_OLD_ENTRY = { CONF_HOST: "fake_host", CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", - CONF_IP_ADDRESS: "fake_ip_old", + CONF_IP_ADDRESS: EXISTING_IP, + CONF_METHOD: "legacy", + CONF_PORT: None, +} +MOCK_LEGACY_ENTRY = { + CONF_HOST: EXISTING_IP, + CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", CONF_METHOD: "legacy", CONF_PORT: None, } @@ -316,8 +322,8 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): assert result["step_id"] == "confirm" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.try_connect", - return_value=RESULT_SUCCESS, + "homeassistant.components.samsungtv.bridge.Remote.__enter__", + return_value=True, ): # entry was added @@ -885,7 +891,7 @@ async def test_update_old_entry(hass: HomeAssistant, remote: Mock): assert len(config_entries_domain) == 1 assert entry is config_entries_domain[0] assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" - assert entry.data[CONF_IP_ADDRESS] == "fake_ip_old" + assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP assert not entry.unique_id assert await async_setup_component(hass, DOMAIN, {}) is True @@ -1016,6 +1022,69 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" +async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock): + """Test missing mac added.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_LEGACY_ENTRY, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mock): + """Test missing mac added when there is no unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_LEGACY_ENTRY, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS.rest_device_info", + side_effect=HttpApiError, + ), patch( + "homeassistant.components.samsungtv.bridge.Remote.__enter__", + return_value=True, + ), patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id is None + + async def test_form_reauth_legacy(hass, remote: Mock): """Test reauthenticate legacy.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) @@ -1086,9 +1155,6 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): ) await hass.async_block_till_done() - import pprint - - pprint.pprint(result2) assert result2["type"] == "form" assert result2["errors"] == {"base": RESULT_AUTH_MISSING} From 00741d4273679ab48715260e39f48e529e675919 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 08:57:51 -0400 Subject: [PATCH 309/818] Use entity class attributes for agent_dvr (#52501) * Use entity class attributes for agent_dvr * Apply suggestions from code review Co-authored-by: Milan Meulemans * rework Co-authored-by: Milan Meulemans --- .../agent_dvr/alarm_control_panel.py | 70 ++++++------------- homeassistant/components/agent_dvr/camera.py | 67 +++++------------- 2 files changed, 38 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 3e093ae46a8..8f139af8963 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -35,90 +35,60 @@ async def async_setup_entry( class AgentBaseStation(AlarmControlPanelEntity): """Representation of an Agent DVR Alarm Control Panel.""" + _attr_icon = ICON + _attr_supported_features = ( + SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + ) + def __init__(self, client): """Initialize the alarm control panel.""" - self._state = None self._client = client - self._unique_id = f"{client.unique}_CP" - name = CONST_ALARM_CONTROL_PANEL_NAME - self._name = name = f"{client.name} {name}" - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT - - @property - def device_info(self): - """Return the device info for adding the entity to the agent object.""" - return { - "identifiers": {(AGENT_DOMAIN, self._client.unique)}, + self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}" + self._attr_unique_id = f"{client.unique}_CP" + self._attr_device_info = { + "identifiers": {(AGENT_DOMAIN, client.unique)}, "manufacturer": "Agent", "model": CONST_ALARM_CONTROL_PANEL_NAME, - "sw_version": self._client.version, + "sw_version": client.version, } async def async_update(self): """Update the state of the device.""" await self._client.update() + self._attr_available = self._client.is_available armed = self._client.is_armed if armed is None: - self._state = None + self._attr_state = None return if armed: prof = (await self._client.get_active_profile()).lower() - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY if prof == CONF_HOME_MODE_NAME: - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME elif prof == CONF_NIGHT_MODE_NAME: - self._state = STATE_ALARM_ARMED_NIGHT + self._attr_state = STATE_ALARM_ARMED_NIGHT else: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self._client.disarm() - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED async def async_alarm_arm_away(self, code=None): """Send arm away command. Uses custom mode.""" await self._client.arm() await self._client.set_active_profile(CONF_AWAY_MODE_NAME) - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY async def async_alarm_arm_home(self, code=None): """Send arm home command. Uses custom mode.""" await self._client.arm() await self._client.set_active_profile(CONF_HOME_MODE_NAME) - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME async def async_alarm_arm_night(self, code=None): """Send arm night command. Uses custom mode.""" await self._client.arm() await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) - self._state = STATE_ALARM_ARMED_NIGHT - - @property - def name(self): - """Return the name of the base station.""" - return self._name - - @property - def available(self) -> bool: - """Device available.""" - return self._client.is_available - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id + self._attr_state = STATE_ALARM_ARMED_NIGHT diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 6b2363f50d5..30c27eb047a 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -67,31 +67,27 @@ async def async_setup_entry( class AgentCamera(MjpegCamera): """Representation of an Agent Device Stream.""" + _attr_supported_features = SUPPORT_ON_OFF + def __init__(self, device): """Initialize as a subclass of MjpegCamera.""" - self._servername = device.client.name - self.server_url = device.client._server_url - device_info = { CONF_NAME: device.name, - CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", - CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + CONF_MJPEG_URL: f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + CONF_STILL_IMAGE_URL: f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", } self.device = device self._removed = False - self._name = f"{self._servername} {device.name}" - self._unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" + self._attr_name = f"{device.client.name} {device.name}" + self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" + self._attr_should_poll = True super().__init__(device_info) - - @property - def device_info(self): - """Return the device info for adding the entity to the agent object.""" - return { - "identifiers": {(AGENT_DOMAIN, self._unique_id)}, - "name": self._name, + self._attr_device_info = { + "identifiers": {(AGENT_DOMAIN, self.unique_id)}, + "name": self.name, "manufacturer": "Agent", "model": "Camera", - "sw_version": self.device.client.version, + "sw_version": device.client.version, } async def async_update(self): @@ -99,18 +95,18 @@ class AgentCamera(MjpegCamera): try: await self.device.update() if self._removed: - _LOGGER.debug("%s reacquired", self._name) + _LOGGER.debug("%s reacquired", self.name) self._removed = False except AgentError: # server still available - camera error if self.device.client.is_available and not self._removed: - _LOGGER.error("%s lost", self._name) + _LOGGER.error("%s lost", self.name) self._removed = True - - @property - def extra_state_attributes(self): - """Return the Agent DVR camera state attributes.""" - return { + self._attr_available = self.device.client.is_available + self._attr_icon = "mdi:camcorder-off" + if self.is_on: + self._attr_icon = "mdi:camcorder" + self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, "editable": False, "enabled": self.is_on, @@ -121,11 +117,6 @@ class AgentCamera(MjpegCamera): "alerts_enabled": self.device.alerts_active, } - @property - def should_poll(self) -> bool: - """Update the state periodically.""" - return True - @property def is_recording(self) -> bool: """Return whether the monitor is recording.""" @@ -141,43 +132,21 @@ class AgentCamera(MjpegCamera): """Return whether the monitor has alerted.""" return self.device.detected - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.device.client.is_available - @property def connected(self) -> bool: """Return True if entity is connected.""" return self.device.connected - @property - def supported_features(self) -> int: - """Return supported features.""" - return SUPPORT_ON_OFF - @property def is_on(self) -> bool: """Return true if on.""" return self.device.online - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - if self.is_on: - return "mdi:camcorder" - return "mdi:camcorder-off" - @property def motion_detection_enabled(self): """Return the camera motion detection status.""" return self.device.detector_active - @property - def unique_id(self) -> str: - """Return a unique identifier for this agent object.""" - return self._unique_id - async def async_enable_alerts(self): """Enable alerts.""" await self.device.alerts_on() From 35cab74be6474eac836ded97d1b37737f208dfb1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 09:29:12 -0400 Subject: [PATCH 310/818] Use entity class attributes for Bloomsky (#53030) Co-authored-by: Tobias Sauerwein --- .../components/bloomsky/binary_sensor.py | 30 +++------------ homeassistant/components/bloomsky/camera.py | 13 +------ homeassistant/components/bloomsky/sensor.py | 37 ++++--------------- 3 files changed, 15 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 4234b4fb145..858c39c4db9 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -44,32 +44,14 @@ class BloomSkySensor(BinarySensorEntity): self._bloomsky = bs self._device_id = device["DeviceID"] self._sensor_name = sensor_name - self._name = f"{device['DeviceName']} {sensor_name}" - self._state = None - self._unique_id = f"{self._device_id}-{self._sensor_name}" - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the BloomSky device and this sensor.""" - return self._name - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return SENSOR_TYPES.get(self._sensor_name) - - @property - def is_on(self): - """Return true if binary sensor is on.""" - return self._state + self._attr_name = f"{device['DeviceName']} {sensor_name}" + self._attr_unique_id = f"{self._device_id}-{sensor_name}" + self._attr_device_class = SENSOR_TYPES.get(sensor_name) def update(self): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() - self._state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] + self._attr_is_on = self._bloomsky.devices[self._device_id]["Data"][ + self._sensor_name + ] diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index e14e2f5c68b..570842b9c66 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -25,7 +25,7 @@ class BloomSkyCamera(Camera): def __init__(self, bs, device): """Initialize access to the BloomSky camera images.""" super().__init__() - self._name = device["DeviceName"] + self._attr_name = device["DeviceName"] self._id = device["DeviceID"] self._bloomsky = bs self._url = "" @@ -35,6 +35,7 @@ class BloomSkyCamera(Camera): # to download the same image over and over. self._last_image = "" self._logger = logging.getLogger(__name__) + self._attr_unique_id = self._id def camera_image(self): """Update the camera's image if it has changed.""" @@ -51,13 +52,3 @@ class BloomSkyCamera(Camera): return None return self._last_image - - @property - def unique_id(self): - """Return a unique ID.""" - return self._id - - @property - def name(self): - """Return the name of this BloomSky device.""" - return self._name diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index cf494c40916..29ca198c1fc 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -83,31 +83,11 @@ class BloomSkySensor(SensorEntity): self._bloomsky = bs self._device_id = device["DeviceID"] self._sensor_name = sensor_name - self._name = f"{device['DeviceName']} {sensor_name}" - self._state = None - self._unique_id = f"{self._device_id}-{self._sensor_name}" - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the BloomSky device and this sensor.""" - return self._name - - @property - def state(self): - """Return the current state, eg. value, of this sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the sensor units.""" + self._attr_name = f"{device['DeviceName']} {sensor_name}" + self._attr_unique_id = f"{self._device_id}-{sensor_name}" + self._attr_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get(sensor_name, None) if self._bloomsky.is_metric: - return SENSOR_UNITS_METRIC.get(self._sensor_name, None) - return SENSOR_UNITS_IMPERIAL.get(self._sensor_name, None) + self._attr_unit_of_measurement = SENSOR_UNITS_METRIC.get(sensor_name, None) @property def device_class(self): @@ -117,10 +97,7 @@ class BloomSkySensor(SensorEntity): def update(self): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() - state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] - - if self._sensor_name in FORMAT_NUMBERS: - self._state = f"{state:.2f}" - else: - self._state = state + self._attr_state = ( + f"{state:.2f}" if self._sensor_name in FORMAT_NUMBERS else state + ) From c7b61fd8cebf36f7537ee6beece4124ece7c0c3d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 09:35:42 -0400 Subject: [PATCH 311/818] Use entity class attributes for androidtv (#52531) * Use entity class attributes for androidtv * fix * fix pylint * fix --- .../components/androidtv/media_player.py | 194 +++++------------- 1 file changed, 53 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 5db73d14914..7b901e7ab37 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -80,7 +80,9 @@ SUPPORT_FIRETV = ( | SUPPORT_STOP ) +ATTR_ADB_RESPONSE = "adb_response" ATTR_DEVICE_PATH = "device_path" +ATTR_HDMI_INPUT = "hdmi_input" ATTR_LOCAL_PATH = "local_path" CONF_ADBKEY = "adbkey" @@ -374,13 +376,13 @@ def adb_decorator(override_available=False): err, ) await self.aftv.adb_close() - self._available = False + self._attr_available = False # pylint: disable=protected-access return None except Exception: # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again, then raise the exception. await self.aftv.adb_close() - self._available = False + self._attr_available = False # pylint: disable=protected-access raise return _adb_exception_catcher @@ -404,7 +406,7 @@ class ADBDevice(MediaPlayerEntity): ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv - self._name = name + self._attr_name = name self._app_id_to_name = APPS.copy() self._app_id_to_name.update(apps) self._app_name_to_id = { @@ -415,12 +417,8 @@ class ADBDevice(MediaPlayerEntity): # in `self._app_name_to_id` for key, value in apps.items(): self._app_name_to_id[value] = key - self._get_sources = get_sources - self._keys = KEYS - - self._device_properties = self.aftv.device_properties - self._unique_id = self._device_properties.get("serialno") + self._attr_unique_id = self.aftv.device_properties.get("serialno") self.turn_on_command = turn_on_command self.turn_off_command = turn_off_command @@ -446,66 +444,11 @@ class ADBDevice(MediaPlayerEntity): self.exceptions = (ConnectionResetError, RuntimeError) # Property attributes - self._adb_response = None - self._available = True - self._current_app = None - self._sources = None - self._state = None - self._hdmi_input = None - - @property - def app_id(self): - """Return the current app.""" - return self._current_app - - @property - def app_name(self): - """Return the friendly name of the current app.""" - return self._app_id_to_name.get(self._current_app, self._current_app) - - @property - def available(self): - """Return whether or not the ADB connection is valid.""" - return self._available - - @property - def extra_state_attributes(self): - """Provide the last ADB command's response and the device's HDMI input as attributes.""" - return { - "adb_response": self._adb_response, - "hdmi_input": self._hdmi_input, + self._attr_extra_state_attributes = { + ATTR_ADB_RESPONSE: None, + ATTR_HDMI_INPUT: None, } - @property - def media_image_hash(self): - """Hash value for media image.""" - return f"{datetime.now().timestamp()}" if self._screencap else None - - @property - def name(self): - """Return the device name.""" - return self._name - - @property - def source(self): - """Return the current app.""" - return self._app_id_to_name.get(self._current_app, self._current_app) - - @property - def source_list(self): - """Return a list of running apps.""" - return self._sources - - @property - def state(self): - """Return the state of the player.""" - return self._state - - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - @adb_decorator() async def _adb_screencap(self): """Take a screen capture from the device.""" @@ -515,6 +458,9 @@ class ADBDevice(MediaPlayerEntity): """Fetch current playing image.""" if not self._screencap or self.state in [STATE_OFF, None] or not self.available: return None, None + self._attr_media_image_hash = ( + f"{datetime.now().timestamp()}" if self._screencap else None + ) media_data = await self._adb_screencap() if media_data: @@ -584,15 +530,17 @@ class ADBDevice(MediaPlayerEntity): @adb_decorator() async def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" - key = self._keys.get(cmd) + key = KEYS.get(cmd) if key: await self.aftv.adb_shell(f"input keyevent {key}") return if cmd == "GET_PROPERTIES": - self._adb_response = str(await self.aftv.get_properties_dict()) + self._attr_extra_state_attributes[ATTR_ADB_RESPONSE] = str( + await self.aftv.get_properties_dict() + ) self.async_write_ha_state() - return self._adb_response + return try: response = await self.aftv.adb_shell(cmd) @@ -600,17 +548,17 @@ class ADBDevice(MediaPlayerEntity): return if isinstance(response, str) and response.strip(): - self._adb_response = response.strip() + self._attr_extra_state_attributes[ATTR_ADB_RESPONSE] = response.strip() self.async_write_ha_state() - return self._adb_response + return @adb_decorator() async def learn_sendevent(self): """Translate a key press on a remote to ADB 'sendevent' commands.""" output = await self.aftv.learn_sendevent() if output: - self._adb_response = output + self._attr_extra_state_attributes[ATTR_ADB_RESPONSE] = output self.async_write_ha_state() msg = f"Output from service '{SERVICE_LEARN_SENDEVENT}' from {self.entity_id}: '{output}'" @@ -634,84 +582,48 @@ class ADBDevice(MediaPlayerEntity): class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" - def __init__( - self, - aftv, - name, - apps, - get_sources, - turn_on_command, - turn_off_command, - exclude_unnamed_apps, - screencap, - ): - """Initialize the Android TV device.""" - super().__init__( - aftv, - name, - apps, - get_sources, - turn_on_command, - turn_off_command, - exclude_unnamed_apps, - screencap, - ) - - self._is_volume_muted = None - self._volume_level = None + _attr_supported_features = SUPPORT_ANDROIDTV @adb_decorator(override_available=True) async def async_update(self): """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. - if not self._available: + if not self.available: # Try to connect - self._available = await self.aftv.adb_connect(always_log_errors=False) + self._attr_available = await self.aftv.adb_connect(always_log_errors=False) # If the ADB connection is not intact, don't update. - if not self._available: + if not self.available: return # Get the updated state and attributes. ( state, - self._current_app, + self._attr_app_id, running_apps, _, - self._is_volume_muted, - self._volume_level, - self._hdmi_input, + self._attr_is_volume_muted, + self._attr_volume_level, + self._attr_extra_state_attributes[ATTR_HDMI_INPUT], ) = await self.aftv.update(self._get_sources) - self._state = ANDROIDTV_STATES.get(state) - if self._state is None: - self._available = False + self._attr_state = ANDROIDTV_STATES.get(state) + if self._attr_state is None: + self._attr_available = False if running_apps: + self._attr_source = self._attr_app_name = self._app_id_to_name.get( + self._attr_app_id, self._attr_app_id + ) sources = [ self._app_id_to_name.get( app_id, app_id if not self._exclude_unnamed_apps else None ) for app_id in running_apps ] - self._sources = [source for source in sources if source] + self._attr_source_list = [source for source in sources if source] else: - self._sources = None - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._is_volume_muted - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_ANDROIDTV - - @property - def volume_level(self): - """Return the volume level.""" - return self._volume_level + self._attr_source_list = None @adb_decorator() async def async_media_stop(self): @@ -731,56 +643,56 @@ class AndroidTVDevice(ADBDevice): @adb_decorator() async def async_volume_down(self): """Send volume down command.""" - self._volume_level = await self.aftv.volume_down(self._volume_level) + self._attr_volume_level = await self.aftv.volume_down(self._attr_volume_level) @adb_decorator() async def async_volume_up(self): """Send volume up command.""" - self._volume_level = await self.aftv.volume_up(self._volume_level) + self._attr_volume_level = await self.aftv.volume_up(self._attr_volume_level) class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" + _attr_supported_features = SUPPORT_FIRETV + @adb_decorator(override_available=True) async def async_update(self): """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. - if not self._available: + if not self.available: # Try to connect - self._available = await self.aftv.adb_connect(always_log_errors=False) + self._attr_available = await self.aftv.adb_connect(always_log_errors=False) # If the ADB connection is not intact, don't update. - if not self._available: + if not self.available: return # Get the `state`, `current_app`, `running_apps` and `hdmi_input`. ( state, - self._current_app, + self._attr_app_id, running_apps, - self._hdmi_input, + self._attr_extra_state_attributes[ATTR_HDMI_INPUT], ) = await self.aftv.update(self._get_sources) - self._state = ANDROIDTV_STATES.get(state) - if self._state is None: - self._available = False + self._attr_state = ANDROIDTV_STATES.get(state) + if self._attr_state is None: + self._attr_available = False if running_apps: + self._attr_source = self._app_id_to_name.get( + self._attr_app_id, self._attr_app_id + ) sources = [ self._app_id_to_name.get( app_id, app_id if not self._exclude_unnamed_apps else None ) for app_id in running_apps ] - self._sources = [source for source in sources if source] + self._attr_source_list = [source for source in sources if source] else: - self._sources = None - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_FIRETV + self._attr_source_list = None @adb_decorator() async def async_media_stop(self): From d30ed05f305afd813dcffef85865ba7e30a8c702 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Jul 2021 10:27:18 -0700 Subject: [PATCH 312/818] Expose Spotify as a service (#53063) --- homeassistant/components/spotify/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 1c92e2ce51a..c88aa453d2c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -266,6 +266,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): "manufacturer": "Spotify AB", "model": model, "name": self._name, + "entry_type": "service", } @property From 989839a1a9fb4540265eba2036e2325c4dd44770 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Jul 2021 10:57:06 -0700 Subject: [PATCH 313/818] Generate const files for config flow scaffolds (#53064) --- script/scaffold/templates/config_flow/integration/const.py | 3 +++ .../templates/config_flow_discovery/integration/const.py | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 script/scaffold/templates/config_flow/integration/const.py create mode 100644 script/scaffold/templates/config_flow_discovery/integration/const.py diff --git a/script/scaffold/templates/config_flow/integration/const.py b/script/scaffold/templates/config_flow/integration/const.py new file mode 100644 index 00000000000..e8a1c494d49 --- /dev/null +++ b/script/scaffold/templates/config_flow/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" diff --git a/script/scaffold/templates/config_flow_discovery/integration/const.py b/script/scaffold/templates/config_flow_discovery/integration/const.py new file mode 100644 index 00000000000..e8a1c494d49 --- /dev/null +++ b/script/scaffold/templates/config_flow_discovery/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" From cf647c5d20a6d4952842ba2f3e3150c92ff0d560 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Thu, 15 Jul 2021 23:24:54 +0200 Subject: [PATCH 314/818] Increase polling interval to prevent reaching daily limit (#53066) * increase polling interval to prevent reaching daily limit * update test accordingly --- homeassistant/components/home_plus_control/__init__.py | 2 +- tests/components/home_plus_control/test_switch.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 954203e9b10..718900533aa 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="home_plus_control_module", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=300), ) hass_entry_data[DATA_COORDINATOR] = coordinator diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index aec23f0d32a..75d416ba2b1 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -146,7 +146,7 @@ async def test_plant_topology_reduction_change( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -208,7 +208,7 @@ async def test_plant_topology_increase_change( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -268,7 +268,7 @@ async def test_module_status_unavailable(hass, mock_config_entry, mock_modules): return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_module_status_available( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -443,7 +443,7 @@ async def test_update_with_api_error( side_effect=HomePlusControlApiError, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 From 62a2efaf27926d4ce47982476048dffc0a3b69f6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 16 Jul 2021 00:10:34 +0000 Subject: [PATCH 315/818] [ci skip] Translation update --- .../accuweather/translations/ar.json | 23 +++++++++++++++- .../accuweather/translations/sensor.ar.json | 9 +++++++ .../components/aemet/translations/ar.json | 11 ++++++++ .../components/coinbase/translations/en.json | 4 ++- .../components/coinbase/translations/et.json | 1 + .../components/coinbase/translations/ru.json | 1 + .../coinbase/translations/zh-Hant.json | 1 + .../met_eireann/translations/ar.json | 9 +++++++ .../components/metoffice/translations/ar.json | 9 +++++++ .../components/netatmo/translations/ar.json | 27 +++++++++++++++++++ .../components/nws/translations/ar.json | 9 +++++++ .../openweathermap/translations/ar.json | 23 ++++++++++++++++ .../components/weather/translations/ar.json | 16 ++++++++--- .../components/zha/translations/nl.json | 2 +- .../components/zwave_js/translations/ca.json | 5 ++++ .../components/zwave_js/translations/ru.json | 8 ++++++ .../zwave_js/translations/zh-Hant.json | 8 ++++++ 17 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.ar.json create mode 100644 homeassistant/components/aemet/translations/ar.json create mode 100644 homeassistant/components/met_eireann/translations/ar.json create mode 100644 homeassistant/components/metoffice/translations/ar.json create mode 100644 homeassistant/components/netatmo/translations/ar.json create mode 100644 homeassistant/components/nws/translations/ar.json create mode 100644 homeassistant/components/openweathermap/translations/ar.json diff --git a/homeassistant/components/accuweather/translations/ar.json b/homeassistant/components/accuweather/translations/ar.json index bd0c7cf77ba..0694e096019 100644 --- a/homeassistant/components/accuweather/translations/ar.json +++ b/homeassistant/components/accuweather/translations/ar.json @@ -1,9 +1,30 @@ { + "config": { + "error": { + "requests_exceeded": "\u062a\u0645 \u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u0639\u062f\u062f \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647 \u0645\u0646 \u0627\u0644\u0637\u0644\u0628\u0627\u062a \u0625\u0644\u0649 Accuweather API. \u0639\u0644\u064a\u0643 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0623\u0648 \u062a\u063a\u064a\u064a\u0631 \u0645\u0641\u062a\u0627\u062d API." + }, + "step": { + "user": { + "description": "\u0625\u0630\u0627 \u0643\u0646\u062a \u0628\u062d\u0627\u062c\u0629 \u0625\u0644\u0649 \u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u060c \u0641\u0642\u0645 \u0628\u0625\u0644\u0642\u0627\u0621 \u0646\u0638\u0631\u0629 \u0647\u0646\u0627: https://www.home-assistant.io/integrations/accuweather/ \n\n \u0644\u0627 \u064a\u062a\u0645 \u062a\u0645\u0643\u064a\u0646 \u0628\u0639\u0636 \u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0628\u0634\u0643\u0644 \u0627\u0641\u062a\u0631\u0627\u0636\u064a. \u064a\u0645\u0643\u0646\u0643 \u062a\u0645\u0643\u064a\u0646\u0647\u0645 \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u0643\u064a\u0627\u0646 \u0628\u0639\u062f \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062a\u0643\u0627\u0645\u0644.\n \u0644\u0627 \u064a\u062a\u0645 \u062a\u0645\u0643\u064a\u0646 \u062a\u0648\u0642\u0639\u0627\u062a \u0627\u0644\u0637\u0642\u0633 \u0627\u0641\u062a\u0631\u0627\u0636\u064a\u064b\u0627. \u064a\u0645\u0643\u0646\u0643 \u062a\u0645\u0643\u064a\u0646\u0647 \u0641\u064a \u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062a\u0643\u0627\u0645\u0644.", + "title": "AccuWeather" + } + } + }, "options": { "step": { "user": { - "description": "\u0646\u0638\u0631\u064b\u0627 \u0644\u0642\u064a\u0648\u062f \u0627\u0644\u0625\u0635\u062f\u0627\u0631 \u0627\u0644\u0645\u062c\u0627\u0646\u064a \u0645\u0646 \u0645\u0641\u062a\u0627\u062d AccuWeather API \u060c \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0646\u0628\u0624 \u0628\u0627\u0644\u0637\u0642\u0633 \u060c \u0633\u064a\u062a\u0645 \u0625\u062c\u0631\u0627\u0621 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0643\u0644 80 \u062f\u0642\u064a\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0643\u0644 40 \u062f\u0642\u064a\u0642\u0629." + "data": { + "forecast": "\u0627\u0644\u0646\u0634\u0631\u0629 \u0627\u0644\u062c\u0648\u064a\u0629" + }, + "description": "\u0646\u0638\u0631\u064b\u0627 \u0644\u0642\u064a\u0648\u062f \u0627\u0644\u0625\u0635\u062f\u0627\u0631 \u0627\u0644\u0645\u062c\u0627\u0646\u064a \u0645\u0646 \u0645\u0641\u062a\u0627\u062d AccuWeather API \u060c \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0646\u0628\u0624 \u0628\u0627\u0644\u0637\u0642\u0633 \u060c \u0633\u064a\u062a\u0645 \u0625\u062c\u0631\u0627\u0621 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0643\u0644 80 \u062f\u0642\u064a\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0643\u0644 40 \u062f\u0642\u064a\u0642\u0629.", + "title": "\u062e\u064a\u0627\u0631\u0627\u062a AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u062e\u0627\u062f\u0645 AccuWeather", + "remaining_requests": "\u0627\u0644\u0637\u0644\u0628\u0627\u062a \u0627\u0644\u0645\u062a\u0628\u0642\u064a\u0629 \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647\u0627" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.ar.json b/homeassistant/components/accuweather/translations/sensor.ar.json new file mode 100644 index 00000000000..948bd1c95a2 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.ar.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u0647\u0628\u0648\u0637", + "rising": "\u0627\u0631\u062a\u0641\u0627\u0639", + "steady": "\u062b\u0627\u0628\u062a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/ar.json b/homeassistant/components/aemet/translations/ar.json new file mode 100644 index 00000000000..68ba2eda2f2 --- /dev/null +++ b/homeassistant/components/aemet/translations/ar.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "station_updates": "\u062c\u0645\u0639 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0646 \u0645\u062d\u0637\u0627\u062a \u0627\u0644\u0637\u0642\u0633 AEMET" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index e20f5f2e264..0c5b296bce0 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -12,7 +12,9 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret" + "api_token": "API Secret", + "currencies": "Account Balance Currencies", + "exchange_rates": "Exchange Rates" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json index ce5ea46ce34..84673940cab 100644 --- a/homeassistant/components/coinbase/translations/et.json +++ b/homeassistant/components/coinbase/translations/et.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Rahakoti saldod teavitamine.", + "exchange_base": "Vahetuskursiandurite baasvaluuta.", "exchange_rate_currencies": "Vahetuskursside aruanne." }, "description": "Kohanda Coinbase'i valikuid" diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json index 93bb203d24b..965cc5d0b96 100644 --- a/homeassistant/components/coinbase/translations/ru.json +++ b/homeassistant/components/coinbase/translations/ru.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "\u0411\u0430\u043b\u0430\u043d\u0441\u044b \u043a\u043e\u0448\u0435\u043b\u044c\u043a\u0430 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438.", + "exchange_base": "\u0411\u0430\u0437\u043e\u0432\u0430\u044f \u0432\u0430\u043b\u044e\u0442\u0430 \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u043e\u0431\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u0443\u0440\u0441\u0430.", "exchange_rate_currencies": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438." }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 Coinbase" diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index 1ea37aa68ad..5db1da7d23b 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "\u5e33\u6236\u9918\u984d\u56de\u5831\u503c\u3002", + "exchange_base": "\u532f\u7387\u50b3\u611f\u5668\u57fa\u6e96\u8ca8\u5e63\u3002", "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002" }, "description": "\u8abf\u6574 Coinbase \u9078\u9805" diff --git a/homeassistant/components/met_eireann/translations/ar.json b/homeassistant/components/met_eireann/translations/ar.json new file mode 100644 index 00000000000..22ba080793b --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u0623\u062f\u062e\u0644 \u0645\u0648\u0642\u0639\u0643 \u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0637\u0642\u0633 \u0645\u0646 Met \u00c9ireann Public Weather Forecast API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/ar.json b/homeassistant/components/metoffice/translations/ar.json new file mode 100644 index 00000000000..32617bb5576 --- /dev/null +++ b/homeassistant/components/metoffice/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u0633\u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u062e\u0637\u0648\u0637 \u0627\u0644\u0637\u0648\u0644 \u0648\u0627\u0644\u0639\u0631\u0636 \u0644\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u0642\u0631\u0628 \u0645\u062d\u0637\u0629 \u0637\u0642\u0633." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ar.json b/homeassistant/components/netatmo/translations/ar.json new file mode 100644 index 00000000000..2555a59177b --- /dev/null +++ b/homeassistant/components/netatmo/translations/ar.json @@ -0,0 +1,27 @@ +{ + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0646\u0637\u0642\u0629", + "lat_ne": " \u0627\u0644\u0632\u0627\u0648\u064a\u0629 \u0627\u0644\u0634\u0645\u0627\u0644\u064a\u0629 \u0627\u0644\u0634\u0631\u0642\u064a\u0629", + "lat_sw": " \u0627\u0644\u0632\u0627\u0648\u064a\u0629 \u0627\u0644\u062c\u0646\u0648\u0628\u064a\u0629 \u0627\u0644\u063a\u0631\u0628\u064a\u0629", + "lon_ne": " \u0627\u0644\u0632\u0627\u0648\u064a\u0629 \u0627\u0644\u0634\u0645\u0627\u0644\u064a\u0629 \u0627\u0644\u0634\u0631\u0642\u064a\u0629", + "lon_sw": " \u0627\u0644\u0632\u0627\u0648\u064a\u0629 \u0627\u0644\u062c\u0646\u0648\u0628\u064a\u0629 \u0627\u0644\u063a\u0631\u0628\u064a\u0629", + "mode": "\u062d\u0633\u0627\u0628", + "show_on_map": "\u0639\u0631\u0636 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629" + }, + "description": "\u062a\u0643\u0648\u064a\u0646 \u062c\u0647\u0627\u0632 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0637\u0642\u0633 \u0627\u0644\u0639\u0627\u0645 \u0644\u0645\u0646\u0637\u0642\u0629.", + "title": "\u062c\u0647\u0627\u0632 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0637\u0642\u0633 \u0627\u0644\u0639\u0627\u0645 Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0646\u0637\u0642\u0629", + "weather_areas": "\u0645\u0646\u0627\u0637\u0642 \u0627\u0644\u0637\u0642\u0633" + }, + "description": "\u062a\u0643\u0648\u064a\u0646 \u0623\u062c\u0647\u0632\u0629 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0637\u0642\u0633 \u0627\u0644\u0639\u0627\u0645\u0629.", + "title": "\u062c\u0647\u0627\u0632 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0627\u0644\u0637\u0642\u0633 \u0627\u0644\u0639\u0627\u0645 Netatmo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/ar.json b/homeassistant/components/nws/translations/ar.json new file mode 100644 index 00000000000..98ff43c0aec --- /dev/null +++ b/homeassistant/components/nws/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u062e\u062f\u0645\u0629 \u0627\u0644\u0623\u0631\u0635\u0627\u062f \u0627\u0644\u062c\u0648\u064a\u0629 \u0627\u0644\u0648\u0637\u0646\u064a\u0629" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/ar.json b/homeassistant/components/openweathermap/translations/ar.json new file mode 100644 index 00000000000..1f69a1cf739 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/ar.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "language": "\u0627\u0644\u0644\u063a\u0629", + "name": "\u0627\u0633\u0645 \u0627\u0644\u062a\u0643\u0627\u0645\u0644" + }, + "description": "\u0642\u0645 \u0628\u0625\u0639\u062f\u0627\u062f \u062a\u0643\u0627\u0645\u0644 OpenWeatherMap. \u0644\u0625\u0646\u0634\u0627\u0621 \u0645\u0641\u062a\u0627\u062d API \u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649 https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u0627\u0644\u0644\u063a\u0629" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/ar.json b/homeassistant/components/weather/translations/ar.json index 89e6cddf113..aa816d118e8 100644 --- a/homeassistant/components/weather/translations/ar.json +++ b/homeassistant/components/weather/translations/ar.json @@ -1,11 +1,21 @@ { "state": { "_": { - "cloudy": "Bewolkt", - "fog": "Mist", + "clear-night": "\u0644\u064a\u0644\u0629 \u0635\u0627\u0641\u064a\u0629", + "cloudy": "\u063a\u0627\u0626\u0645", + "exceptional": "\u0627\u0633\u062a\u062b\u0646\u0627\u0626\u064a", + "fog": "\u0636\u0628\u0627\u0628", + "hail": "\u0628\u0631\u062f", "lightning": "\u0628\u0631\u0642", "lightning-rainy": "\u0628\u0631\u0642 \u060c \u0645\u0627\u0637\u0631", - "sunny": "\u0645\u0634\u0645\u0633" + "partlycloudy": "\u063a\u0627\u0626\u0645 \u062c\u0632\u0626\u064a\u0627", + "pouring": "\u0623\u0645\u0637\u0627\u0631 \u063a\u0632\u064a\u0631\u0629", + "rainy": "\u0645\u0627\u0637\u0631", + "snowy": "\u062b\u0644\u062c\u064a", + "snowy-rainy": "\u062b\u0644\u062c\u064a\u060c \u0645\u0645\u0637\u0631", + "sunny": "\u0645\u0634\u0645\u0633", + "windy": "\u0639\u0627\u0635\u0641", + "windy-variant": "\u0639\u0627\u0635\u0641" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index f4cdb8f642b..9c63b8989cb 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -41,7 +41,7 @@ "title": "Alarm bedieningspaneel Opties" }, "zha_options": { - "consider_unavailable_battery": "Overweeg apparaten met batterijvoeding als onbeschikbaar na (seconden)", + "consider_unavailable_battery": "Beschouw apparaten met batterijvoeding als onbeschikbaar na (seconden)", "consider_unavailable_mains": "Beschouw apparaten op netvoeding als onbeschikbaar na (seconden)", "default_light_transition": "Standaard licht transitietijd (seconden)", "enable_identify_on_join": "Schakel het identificatie-effect in wanneer apparaten in het netwerk komen", diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index cc1e03a40ad..1084f0b4722 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -56,6 +56,11 @@ "config_parameter": "Configura el valor del par\u00e0metre {subtype}", "node_status": "Estat del node", "value": "Valor actual d'un valor Z-Wave" + }, + "trigger_type": { + "event.notification.entry_control": "Ha enviat una notificaci\u00f3 de control d'entrada", + "event.notification.notification": "Ha enviat una notificaci\u00f3", + "state.node_status": "L'estat del node ha canviat" } }, "options": { diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 5dbfd184fdb..03529769828 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -56,6 +56,14 @@ "config_parameter": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {subtype}", "node_status": "\u0421\u0442\u0430\u0442\u0443\u0441 \u0443\u0437\u043b\u0430", "value": "\u0422\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 Z-Wave Value" + }, + "trigger_type": { + "event.notification.entry_control": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435 \u0432\u0445\u043e\u0434\u0430", + "event.notification.notification": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435", + "event.value_notification.basic": "\u0411\u0430\u0437\u043e\u0432\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 CC \u043d\u0430 {subtype}", + "event.value_notification.central_scene": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"\u0426\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0446\u0435\u043d\u0430\" \u043d\u0430 {subtype}", + "event.value_notification.scene_activation": "\u0410\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u044f \u0441\u0446\u0435\u043d\u044b \u043d\u0430 {subtype}", + "state.node_status": "\u0421\u0442\u0430\u0442\u0443\u0441 \u0443\u0437\u043b\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d" } }, "options": { diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index e98b218ebc8..8b1c4caff1f 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -56,6 +56,14 @@ "config_parameter": "\u8a2d\u5b9a\u53c3\u6578 {subtype} \u6578\u503c", "node_status": "\u7bc0\u9ede\u72c0\u614b", "value": "Z-Wave \u76ee\u524d\u503c" + }, + "trigger_type": { + "event.notification.entry_control": "\u50b3\u9001\u5be6\u9ad4\u63a7\u5236\u901a\u77e5", + "event.notification.notification": "\u50b3\u9001\u901a\u77e5", + "event.value_notification.basic": "{subtype} \u4e0a\u57fa\u672c CC \u4e8b\u4ef6", + "event.value_notification.central_scene": "{subtype} \u4e0a\u6838\u5fc3\u5834\u666f\u52d5\u4f5c", + "event.value_notification.scene_activation": "{subtype} \u4e0a\u5834\u666f\u5df2\u555f\u52d5", + "state.node_status": "\u7bc0\u9ede\u72c0\u614b\u5df2\u6539\u8b8a" } }, "options": { From 4075e8373264a4fdc6b0a6949ce5f27d953ca363 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 15 Jul 2021 23:56:57 -0400 Subject: [PATCH 316/818] Fix google test coverage (#53060) --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 6f6adee207a..3e8b0c2679c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,7 +374,6 @@ omit = homeassistant/components/goalzero/binary_sensor.py homeassistant/components/goalzero/sensor.py homeassistant/components/goalzero/switch.py - homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_travel_time/__init__.py From da7b29285561d351d035e264911ff4d96b08799f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Fri, 16 Jul 2021 05:58:32 +0200 Subject: [PATCH 317/818] Use Utility Meter configured name as friendly name (#53051) --- homeassistant/components/utility_meter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 5442cd583e2..25aa6018d44 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -76,7 +76,7 @@ async def async_setup(hass, config): hass, SENSOR_DOMAIN, DOMAIN, - [{CONF_METER: meter, CONF_NAME: meter}], + [{CONF_METER: meter, CONF_NAME: conf.get(CONF_NAME, meter)}], config, ) ) From c6e1f8878d81ac1140e170ca0918fa2778cb681c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Jul 2021 09:03:28 +0200 Subject: [PATCH 318/818] Add light white parameter to light/services.yaml (#53075) --- homeassistant/components/light/services.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index e2a8a94a74a..778203a1c93 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -265,6 +265,17 @@ turn_on: min: -100 max: 100 unit_of_measurement: "%" + white: + name: White + description: + Set the light to white mode and change its brightness, where 0 turns + the light off, 1 is the minimum brightness and 255 is the maximum + brightness supported by the light. + advanced: true + selector: + number: + min: 0 + max: 255 profile: name: Profile description: Name of a light profile to use. From 268c7ef76860d19f9cc125f37a75ba43b015102e Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 16 Jul 2021 11:40:08 +0200 Subject: [PATCH 319/818] Rewrite mocking in devolo Home Control tests (#53011) * Rework mocking * Instantiate properties --- tests/components/devolo_home_control/mocks.py | 104 +++++++++++------- .../devolo_home_control/test_binary_sensor.py | 32 +++--- 2 files changed, 83 insertions(+), 53 deletions(-) diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index d2ba69d9440..7700d30b1dd 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -1,91 +1,119 @@ """Mocks for tests.""" +from typing import Any from unittest.mock import MagicMock +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl +from devolo_home_control_api.properties.binary_sensor_property import ( + BinarySensorProperty, +) +from devolo_home_control_api.properties.settings_property import SettingsProperty from devolo_home_control_api.publisher.publisher import Publisher -class BinarySensorPropertyMock: +class BinarySensorPropertyMock(BinarySensorProperty): """devolo Home Control binary sensor mock.""" - element_uid = "Test" - key_count = 1 - sensor_type = "door" - sub_type = "" - state = False + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self._logger = MagicMock() + self.element_uid = "Test" + self.key_count = 1 + self.sensor_type = "door" + self.sub_type = "" + self.state = False -class SettingsMock: +class SettingsMock(SettingsProperty): """devolo Home Control settings mock.""" - name = "Test" - zone = "Test" + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self._logger = MagicMock() + self.name = "Test" + self.zone = "Test" -class DeviceMock: +class DeviceMock(Zwave): """devolo Home Control device mock.""" - available = True - brand = "devolo" - name = "Test Device" - uid = "Test" - settings_property = {"general_device_settings": SettingsMock()} - - def is_online(self): - """Mock online state of the device.""" - return DeviceMock.available + def __init__(self) -> None: + """Initialize the mock.""" + self.status = 0 + self.brand = "devolo" + self.name = "Test Device" + self.uid = "Test" + self.settings_property = {"general_device_settings": SettingsMock()} class BinarySensorMock(DeviceMock): """devolo Home Control binary sensor device mock.""" - binary_sensor_property = {"Test": BinarySensorPropertyMock()} + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.binary_sensor_property = {"Test": BinarySensorPropertyMock()} class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" - remote_control_property = {"Test": BinarySensorPropertyMock()} + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.remote_control_property = {"Test": BinarySensorPropertyMock()} class DisabledBinarySensorMock(DeviceMock): """devolo Home Control disabled binary sensor device mock.""" - binary_sensor_property = {"devolo.WarningBinaryFI:Test": BinarySensorPropertyMock()} + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.binary_sensor_property = { + "devolo.WarningBinaryFI:Test": BinarySensorPropertyMock() + } -class HomeControlMock: +class HomeControlMock(HomeControl): """devolo Home Control gateway mock.""" - binary_sensor_devices = [] - binary_switch_devices = [] - multi_level_sensor_devices = [] - multi_level_switch_devices = [] - devices = {} - publisher = MagicMock() + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self.devices = {} + self.publisher = MagicMock() - def websocket_disconnect(self): + def websocket_disconnect(self, event: str): """Mock disconnect of the websocket.""" - pass class HomeControlMockBinarySensor(HomeControlMock): """devolo Home Control gateway mock with binary sensor device.""" - binary_sensor_devices = [BinarySensorMock()] - devices = {"Test": BinarySensorMock()} - publisher = Publisher(devices.keys()) - publisher.unregister = MagicMock() + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = {"Test": BinarySensorMock()} + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() class HomeControlMockRemoteControl(HomeControlMock): """devolo Home Control gateway mock with remote control device.""" - devices = {"Test": RemoteControlMock()} - publisher = Publisher(devices.keys()) + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = {"Test": RemoteControlMock()} + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() class HomeControlMockDisabledBinarySensor(HomeControlMock): """devolo Home Control gateway mock with disabled device.""" - binary_sensor_devices = [DisabledBinarySensorMock()] + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = {"Test": DisabledBinarySensorMock()} diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py index 022cd4a1578..32c2e97e7c9 100644 --- a/tests/components/devolo_home_control/test_binary_sensor.py +++ b/tests/components/devolo_home_control/test_binary_sensor.py @@ -9,7 +9,6 @@ from homeassistant.core import HomeAssistant from . import configure_integration from .mocks import ( - DeviceMock, HomeControlMock, HomeControlMockBinarySensor, HomeControlMockDisabledBinarySensor, @@ -21,10 +20,11 @@ from .mocks import ( async def test_binary_sensor(hass: HomeAssistant): """Test setup and state change of a binary sensor device.""" entry = configure_integration(hass) - DeviceMock.available = True + test_gateway = HomeControlMockBinarySensor() + test_gateway.devices["Test"].status = 0 with patch( "homeassistant.components.devolo_home_control.HomeControl", - side_effect=[HomeControlMockBinarySensor, HomeControlMock], + side_effect=[test_gateway, HomeControlMock()], ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -34,13 +34,13 @@ async def test_binary_sensor(hass: HomeAssistant): assert state.state == STATE_OFF # Emulate websocket message: sensor turned on - HomeControlMockBinarySensor.publisher.dispatch("Test", ("Test", True)) + test_gateway.publisher.dispatch("Test", ("Test", True)) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON # Emulate websocket message: device went offline - DeviceMock.available = False - HomeControlMockBinarySensor.publisher.dispatch("Test", ("Status", False, "status")) + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE @@ -49,10 +49,11 @@ async def test_binary_sensor(hass: HomeAssistant): async def test_remote_control(hass: HomeAssistant): """Test setup and state change of a remote control device.""" entry = configure_integration(hass) - DeviceMock.available = True + test_gateway = HomeControlMockRemoteControl() + test_gateway.devices["Test"].status = 0 with patch( "homeassistant.components.devolo_home_control.HomeControl", - side_effect=[HomeControlMockRemoteControl, HomeControlMock], + side_effect=[test_gateway, HomeControlMock()], ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -62,18 +63,18 @@ async def test_remote_control(hass: HomeAssistant): assert state.state == STATE_OFF # Emulate websocket message: button pressed - HomeControlMockRemoteControl.publisher.dispatch("Test", ("Test", 1)) + test_gateway.publisher.dispatch("Test", ("Test", 1)) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON # Emulate websocket message: button released - HomeControlMockRemoteControl.publisher.dispatch("Test", ("Test", 0)) + test_gateway.publisher.dispatch("Test", ("Test", 0)) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF # Emulate websocket message: device went offline - DeviceMock.available = False - HomeControlMockRemoteControl.publisher.dispatch("Test", ("Status", False, "status")) + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE @@ -84,7 +85,7 @@ async def test_disabled(hass: HomeAssistant): entry = configure_integration(hass) with patch( "homeassistant.components.devolo_home_control.HomeControl", - side_effect=[HomeControlMockDisabledBinarySensor, HomeControlMock], + side_effect=[HomeControlMockDisabledBinarySensor(), HomeControlMock()], ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -96,9 +97,10 @@ async def test_disabled(hass: HomeAssistant): async def test_remove_from_hass(hass: HomeAssistant): """Test removing entity.""" entry = configure_integration(hass) + test_gateway = HomeControlMockBinarySensor() with patch( "homeassistant.components.devolo_home_control.HomeControl", - side_effect=[HomeControlMockBinarySensor, HomeControlMock], + side_effect=[test_gateway, HomeControlMock()], ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -109,4 +111,4 @@ async def test_remove_from_hass(hass: HomeAssistant): await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - HomeControlMockBinarySensor.publisher.unregister.assert_called_once() + test_gateway.publisher.unregister.assert_called_once() From 31074ef6b86ff53ce9198b596c2cd1a4863ee4c1 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Fri, 16 Jul 2021 11:46:47 +0200 Subject: [PATCH 320/818] Update name from "generic" to "generic camera" (#53080) --- homeassistant/components/generic/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 8ab7bec48ac..ab6aa18c4d2 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -1,6 +1,6 @@ { "domain": "generic", - "name": "Generic", + "name": "Generic Camera", "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": [], "iot_class": "local_push" From 1a1fcb94f918cd3c33c13e9899dbb3a71cef3e5e Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Fri, 16 Jul 2021 11:56:05 +0200 Subject: [PATCH 321/818] Add cover support to Freedompro (#52723) * Update Freedompro * add new test and fix code * fix test and add support open and close --- .../components/freedompro/__init__.py | 2 +- homeassistant/components/freedompro/cover.py | 117 ++++++++++ tests/components/freedompro/test_cover.py | 200 ++++++++++++++++++ 3 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/freedompro/cover.py create mode 100644 tests/components/freedompro/test_cover.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 311f1794866..96045947814 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "light", "lock", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "light", "lock", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py new file mode 100644 index 00000000000..439887c9626 --- /dev/null +++ b/homeassistant/components/freedompro/cover.py @@ -0,0 +1,117 @@ +"""Support for Freedompro cover.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_WINDOW, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +DEVICE_CLASS_MAP = { + "windowCovering": DEVICE_CLASS_BLIND, + "gate": DEVICE_CLASS_GATE, + "garageDoor": DEVICE_CLASS_GARAGE, + "door": DEVICE_CLASS_DOOR, + "window": DEVICE_CLASS_WINDOW, +} + +SUPPORTED_SENSORS = {"windowCovering", "gate", "garageDoor", "door", "window"} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro cover.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] in SUPPORTED_SENSORS + ) + + +class Device(CoordinatorEntity, CoverEntity): + """Representation of an Freedompro cover.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro cover.""" + super().__init__(coordinator) + self._session = aiohttp_client.async_get_clientsession(hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": device["type"], + "manufacturer": "Freedompro", + } + self._attr_current_cover_position = 0 + self._attr_is_closed = True + self._attr_supported_features = ( + SUPPORT_CLOSE | SUPPORT_OPEN | SUPPORT_SET_POSITION + ) + self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "position" in state: + self._attr_current_cover_position = state["position"] + if self._attr_current_cover_position == 0: + self._attr_is_closed = True + else: + self._attr_is_closed = False + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self.async_set_cover_position(position=100) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + await self.async_set_cover_position(position=0) + + async def async_set_cover_position(self, **kwargs): + """Async function to set position to cover.""" + payload = {} + payload["position"] = kwargs[ATTR_POSITION] + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py new file mode 100644 index 00000000000..d0338dec82c --- /dev/null +++ b/tests/components/freedompro/test_cover.py @@ -0,0 +1,200 @@ +"""Tests for the Freedompro cover.""" +from datetime import timedelta +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.cover import ATTR_POSITION, DOMAIN as COVER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + STATE_CLOSED, + STATE_OPEN, +) +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_get_state( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test states of the cover.""" + init_integration + registry = er.async_get(hass) + registry_device = dr.async_get(hass) + + device = registry_device.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == name + assert device.model == model + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_CLOSED + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + state_response["state"]["position"] = 100 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_set_position( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test set position of the cover.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.cover.put_state") as mock_put_state: + assert await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: [entity_id], ATTR_POSITION: 33}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 33}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_close( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test close cover.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.cover.put_state") as mock_put_state: + assert await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 0}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "cover.blind", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "blind", + "windowCovering", + ) + ], +) +async def test_cover_open( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test open cover.""" + init_integration + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.cover.put_state") as mock_put_state: + assert await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 100}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN From 7d52c30e3630876bb8219b86c00938da28121bb7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 16 Jul 2021 07:13:34 -0400 Subject: [PATCH 322/818] Revert "Fix google test coverage (#53060)" (#53085) This reverts commit 4075e8373264a4fdc6b0a6949ce5f27d953ca363. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 3e8b0c2679c..6f6adee207a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/goalzero/binary_sensor.py homeassistant/components/goalzero/sensor.py homeassistant/components/goalzero/switch.py + homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_travel_time/__init__.py From 3d3db4b0440cb28ffda3a027c8399583c7359af0 Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Fri, 16 Jul 2021 13:38:37 +0200 Subject: [PATCH 323/818] Replace fritz profile switches by per device parental control switches (#52721) * removes old profile switches and add new switches based on new method * use Ellipsis instead of pass * refactor async_add_profile_switches * - add forgotten update_ha_state - add notimplemtederror for devicebase * Update homeassistant/components/fritz/common.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/common.py Co-authored-by: J. Nick Koston * comments * fix for devices that were not connected * Update homeassistant/components/fritz/common.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- homeassistant/components/fritz/common.py | 102 ++++++++-- homeassistant/components/fritz/const.py | 4 - .../components/fritz/device_tracker.py | 97 ++-------- homeassistant/components/fritz/manifest.json | 1 - homeassistant/components/fritz/switch.py | 183 +++++++++++------- requirements_all.txt | 3 - requirements_test_all.txt | 3 - 7 files changed, 212 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index a700554d5d9..707292ceab2 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -15,7 +15,6 @@ from fritzconnection.core.exceptions import ( ) from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus -from fritzprofiles import FritzProfileSwitch, get_all_profiles from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, @@ -24,12 +23,13 @@ from homeassistant.components.device_tracker.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util from .const import ( + DEFAULT_DEVICE_NAME, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USERNAME, @@ -81,12 +81,11 @@ class FritzBoxTools: ) -> None: """Initialize FritzboxTools class.""" self._cancel_scan: CALLBACK_TYPE | None = None - self._devices: dict[str, Any] = {} + self._devices: dict[str, FritzDevice] = {} self._options: MappingProxyType[str, Any] | None = None self._unique_id: str | None = None self.connection: FritzConnection = None self.fritz_hosts: FritzHosts = None - self.fritz_profiles: dict[str, FritzProfileSwitch] = {} self.fritz_status: FritzStatus = None self.hass = hass self.host = host @@ -123,13 +122,6 @@ class FritzBoxTools: self._model = info.get("NewModelName") self._sw_version = info.get("NewSoftwareVersion") - self.fritz_profiles = { - profile: FritzProfileSwitch( - "http://" + self.host, self.username, self.password, profile - ) - for profile in get_all_profiles(self.host, self.username, self.password) - } - async def async_start(self, options: MappingProxyType[str, Any]) -> None: """Start FritzHosts connection.""" self.fritz_hosts = FritzHosts(fc=self.connection) @@ -260,10 +252,92 @@ class FritzData: """Storage class for platform global data.""" tracked: dict = field(default_factory=dict) + profile_switches: dict = field(default_factory=dict) + + +class FritzDeviceBase(Entity): + """Entity base class for a device connected to a FRITZ!Box router.""" + + def __init__(self, router: FritzBoxTools, device: FritzDevice) -> None: + """Initialize a FRITZ!Box device.""" + self._router = router + self._mac: str = device.mac_address + self._name: str = device.hostname or DEFAULT_DEVICE_NAME + + @property + def name(self) -> str: + """Return device name.""" + return self._name + + @property + def ip_address(self) -> str | None: + """Return the primary ip address of the device.""" + if self._mac: + device: FritzDevice = self._router.devices[self._mac] + return device.ip_address + return None + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac + + @property + def hostname(self) -> str | None: + """Return hostname of the device.""" + if self._mac: + device: FritzDevice = self._router.devices[self._mac] + return device.hostname + return None + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + "identifiers": {(DOMAIN, self._mac)}, + "default_name": self.name, + "default_manufacturer": "AVM", + "default_model": "FRITZ!Box Tracked device", + "via_device": ( + DOMAIN, + self._router.unique_id, + ), + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + async def async_process_update(self) -> None: + """Update device.""" + raise NotImplementedError() + + async def async_on_demand_update(self) -> None: + """Update state.""" + await self.async_process_update() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register state update callback.""" + await self.async_process_update() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_device_update, + self.async_on_demand_update, + ) + ) class FritzDevice: - """FritzScanner device.""" + """Representation of a device connected to the FRITZ!Box.""" def __init__(self, mac: str, name: str) -> None: """Initialize device info.""" @@ -292,7 +366,7 @@ class FritzDevice: if dev_home: self._last_activity = utc_point_in_time - self._ip_address = dev_info.ip_address if self._connected else None + self._ip_address = dev_info.ip_address @property def is_connected(self) -> bool: diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 776c7a7dafa..8b3f9106602 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -19,11 +19,7 @@ FRITZ_SERVICES = "fritz_services" SERVICE_REBOOT = "reboot" SERVICE_RECONNECT = "reconnect" -SWITCH_PROFILE_STATUS_OFF = "never" -SWITCH_PROFILE_STATUS_ON = "unlimited" - SWITCH_TYPE_DEFLECTION = "CallDeflection" -SWITCH_TYPE_DEVICEPROFILE = "DeviceProfile" SWITCH_TYPE_PORTFORWARD = "PortForward" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index db6cfadcf5d..e18ec8005cc 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -16,14 +16,12 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from .common import Device, FritzBoxTools, FritzData, FritzDevice -from .const import DATA_FRITZ, DEFAULT_DEVICE_NAME, DOMAIN +from .common import FritzBoxTools, FritzData, FritzDevice, FritzDeviceBase +from .const import DATA_FRITZ, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -93,7 +91,7 @@ def _async_add_entities( ) -> None: """Add new tracker entities from the router.""" - def _is_tracked(mac: str, device: Device) -> bool: + def _is_tracked(mac: str) -> bool: for tracked in data_fritz.tracked.values(): if mac in tracked: return True @@ -105,7 +103,7 @@ def _async_add_entities( data_fritz.tracked[router.unique_id] = set() for mac, device in router.devices.items(): - if device.ip_address == "" or _is_tracked(mac, device): + if device.ip_address == "" or _is_tracked(mac): continue new_tracked.append(FritzBoxTracker(router, device)) @@ -115,14 +113,12 @@ def _async_add_entities( async_add_entities(new_tracked) -class FritzBoxTracker(ScannerEntity): +class FritzBoxTracker(FritzDeviceBase, ScannerEntity): """This class queries a FRITZ!Box router.""" def __init__(self, router: FritzBoxTools, device: FritzDevice) -> None: """Initialize a FRITZ!Box device.""" - self._router = router - self._mac: str = device.mac_address - self._name: str = device.hostname or DEFAULT_DEVICE_NAME + super().__init__(router, device) self._last_activity: datetime.datetime | None = device.last_activity self._active = False @@ -131,59 +127,10 @@ class FritzBoxTracker(ScannerEntity): """Return device status.""" return self._active - @property - def name(self) -> str: - """Return device name.""" - return self._name - @property def unique_id(self) -> str: """Return device unique id.""" - return self._mac - - @property - def ip_address(self) -> str | None: - """Return the primary ip address of the device.""" - if self._mac: - return self._router.devices[self._mac].ip_address # type: ignore[no-any-return] - return None - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self._mac - - @property - def hostname(self) -> str | None: - """Return hostname of the device.""" - if self._mac: - return self._router.devices[self._mac].hostname # type: ignore[no-any-return] - return None - - @property - def source_type(self) -> str: - """Return tracker source type.""" - return SOURCE_TYPE_ROUTER - - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "identifiers": {(DOMAIN, self.unique_id)}, - "default_name": self.name, - "default_manufacturer": "AVM", - "default_model": "FRITZ!Box Tracked device", - "via_device": ( - DOMAIN, - self._router.unique_id, - ), - } - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False + return f"{self._mac}_tracker" @property def icon(self) -> str: @@ -192,11 +139,6 @@ class FritzBoxTracker(ScannerEntity): return "mdi:lan-connect" return "mdi:lan-disconnect" - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - @property def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" @@ -207,8 +149,12 @@ class FritzBoxTracker(ScannerEntity): ) return attrs - @callback - def async_process_update(self) -> None: + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + async def async_process_update(self) -> None: """Update device.""" if not self._mac: return @@ -216,20 +162,3 @@ class FritzBoxTracker(ScannerEntity): device = self._router.devices[self._mac] self._active = device.is_connected self._last_activity = device.last_activity - - @callback - def async_on_demand_update(self) -> None: - """Update state.""" - self.async_process_update() - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Register state update callback.""" - self.async_process_update() - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self._router.signal_device_update, - self.async_on_demand_update, - ) - ) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index d1c096a2ef5..a7c51a69cf7 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ "fritzconnection==1.4.2", - "fritzprofiles==0.6.1", "xmltodict==0.12.0" ], "dependencies": ["network"], diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 238e65feacf..1100a480fc8 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -17,18 +17,24 @@ import xmltodict from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import get_local_ip, slugify -from .common import FritzBoxBaseEntity, FritzBoxTools, SwitchInfo +from .common import ( + FritzBoxBaseEntity, + FritzBoxTools, + FritzData, + FritzDevice, + FritzDeviceBase, + SwitchInfo, +) from .const import ( + DATA_FRITZ, DOMAIN, - SWITCH_PROFILE_STATUS_OFF, - SWITCH_PROFILE_STATUS_ON, SWITCH_TYPE_DEFLECTION, - SWITCH_TYPE_DEVICEPROFILE, SWITCH_TYPE_PORTFORWARD, SWITCH_TYPE_WIFINETWORK, ) @@ -225,21 +231,6 @@ def port_entities_list( return entities_list -def profile_entities_list( - fritzbox_tools: FritzBoxTools, device_friendly_name: str -) -> list[FritzBoxProfileSwitch]: - """Get list of profile entities.""" - _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEVICEPROFILE) - if len(fritzbox_tools.fritz_profiles) <= 0: - _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEVICEPROFILE) - return [] - - return [ - FritzBoxProfileSwitch(fritzbox_tools, device_friendly_name, profile) - for profile in fritzbox_tools.fritz_profiles.keys() - ] - - def wifi_entities_list( fritzbox_tools: FritzBoxTools, device_friendly_name: str ) -> list[FritzBoxWifiSwitch]: @@ -267,15 +258,45 @@ def wifi_entities_list( ] +def profile_entities_list( + router: FritzBoxTools, data_fritz: FritzData +) -> list[FritzBoxProfileSwitch]: + """Add new tracker entities from the router.""" + + def _is_tracked(mac: str) -> bool: + for tracked in data_fritz.profile_switches.values(): + if mac in tracked: + return True + + return False + + new_profiles: list[FritzBoxProfileSwitch] = [] + + if "X_AVM-DE_HostFilter1" not in router.connection.services: + return new_profiles + + if router.unique_id not in data_fritz.profile_switches: + data_fritz.profile_switches[router.unique_id] = set() + + for mac, device in router.devices.items(): + if device.ip_address == "" or _is_tracked(mac): + continue + + new_profiles.append(FritzBoxProfileSwitch(router, device)) + data_fritz.profile_switches[router.unique_id].add(mac) + + return new_profiles + + def all_entities_list( - fritzbox_tools: FritzBoxTools, device_friendly_name: str + fritzbox_tools: FritzBoxTools, device_friendly_name: str, data_fritz: FritzData ) -> list[Entity]: """Get a list of all entities.""" return [ *deflection_entities_list(fritzbox_tools, device_friendly_name), *port_entities_list(fritzbox_tools, device_friendly_name), - *profile_entities_list(fritzbox_tools, device_friendly_name), *wifi_entities_list(fritzbox_tools, device_friendly_name), + *profile_entities_list(fritzbox_tools, data_fritz), ] @@ -285,14 +306,25 @@ async def async_setup_entry( """Set up entry.""" _LOGGER.debug("Setting up switches") fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] + data_fritz: FritzData = hass.data[DATA_FRITZ] _LOGGER.debug("Fritzbox services: %s", fritzbox_tools.connection.services) entities_list = await hass.async_add_executor_job( - all_entities_list, fritzbox_tools, entry.title + all_entities_list, fritzbox_tools, entry.title, data_fritz ) + async_add_entities(entities_list) + @callback + def update_router() -> None: + """Update the values of the router.""" + async_add_entities(profile_entities_list(fritzbox_tools, data_fritz)) + + entry.async_on_unload( + async_dispatcher_connect(hass, fritzbox_tools.signal_device_new, update_router) + ) + class FritzBoxBaseSwitch(FritzBoxBaseEntity): """Fritz switch base class.""" @@ -522,60 +554,67 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity): ) -class FritzBoxProfileSwitch(FritzBoxBaseSwitch, SwitchEntity): +class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): """Defines a FRITZ!Box Tools DeviceProfile switch.""" - def __init__( - self, fritzbox_tools: FritzBoxTools, device_friendly_name: str, profile: str - ) -> None: + def __init__(self, fritzbox_tools: FritzBoxTools, device: FritzDevice) -> None: """Init Fritz profile.""" - self._fritzbox_tools: FritzBoxTools = fritzbox_tools - self.profile = profile - - switch_info = SwitchInfo( - description=f"Profile {profile}", - friendly_name=device_friendly_name, - icon="mdi:router-wireless-settings", - type=SWITCH_TYPE_DEVICEPROFILE, - callback_update=self._async_fetch_update, - callback_switch=self._async_switch_on_off_executor, - ) - super().__init__(self._fritzbox_tools, device_friendly_name, switch_info) - - async def _async_fetch_update(self) -> None: - """Update data.""" - try: - status = await self.hass.async_add_executor_job( - self._fritzbox_tools.fritz_profiles[self.profile].get_state - ) - _LOGGER.debug( - "Specific %s response: get_State()=%s", - SWITCH_TYPE_DEVICEPROFILE, - status, - ) - if status == SWITCH_PROFILE_STATUS_OFF: - self._attr_is_on = False - self._is_available = True - elif status == SWITCH_PROFILE_STATUS_ON: - self._attr_is_on = True - self._is_available = True - else: - self._is_available = False - except Exception: # pylint: disable=broad-except - _LOGGER.error("Could not get %s state", self.name, exc_info=True) - self._is_available = False - - async def _async_switch_on_off_executor(self, turn_on: bool) -> None: - """Handle profile switch.""" - state = SWITCH_PROFILE_STATUS_ON if turn_on else SWITCH_PROFILE_STATUS_OFF - await self.hass.async_add_executor_job( - self._fritzbox_tools.fritz_profiles[self.profile].set_state, state - ) + super().__init__(fritzbox_tools, device) + self._attr_is_on: bool = False @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False + def unique_id(self) -> str: + """Return device unique id.""" + return f"{self._mac}_switch" + + @property + def icon(self) -> str: + """Return device icon.""" + return "mdi:router-wireless-settings" + + async def async_process_update(self) -> None: + """Update device.""" + if not self._mac or not self.ip_address: + return + + wan_disable_info = await async_service_call_action( + self._router, + "X_AVM-DE_HostFilter", + "1", + "GetWANAccessByIP", + NewIPv4Address=self.ip_address, + ) + + if wan_disable_info is None: + return + + self._attr_is_on = not wan_disable_info["NewDisallow"] + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self._async_handle_turn_on_off(turn_on=True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self._async_handle_turn_on_off(turn_on=False) + + async def _async_handle_turn_on_off(self, turn_on: bool) -> bool: + """Handle switch state change request.""" + await self._async_switch_on_off(turn_on) + self._attr_is_on = turn_on + self.async_write_ha_state() + return True + + async def _async_switch_on_off(self, turn_on: bool) -> None: + """Handle parental control switch.""" + await async_service_call_action( + self._router, + "X_AVM-DE_HostFilter", + "1", + "DisallowWANAccessByIP", + NewIPv4Address=self.ip_address, + NewDisallow="0" if turn_on else "1", + ) class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): diff --git a/requirements_all.txt b/requirements_all.txt index e8845193764..538523b1f28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,9 +639,6 @@ freesms==0.2.0 # homeassistant.components.fritzbox_callmonitor fritzconnection==1.4.2 -# homeassistant.components.fritz -fritzprofiles==0.6.1 - # homeassistant.components.google_translate gTTS==2.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f657da146d5..872938f50c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -348,9 +348,6 @@ freebox-api==0.0.10 # homeassistant.components.fritzbox_callmonitor fritzconnection==1.4.2 -# homeassistant.components.fritz -fritzprofiles==0.6.1 - # homeassistant.components.google_translate gTTS==2.2.3 From 6672962f2b6b0d3f39efce42a65cef1ec2edbf31 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Fri, 16 Jul 2021 15:14:37 +0200 Subject: [PATCH 324/818] Add fan support to Freedompro (#52724) * Update Freedompro * Update Freedompro fix async_turn_on * fix test end fix comments * add property is_on * add percent to fan freedompro * fix name rotationSpeed to rotation_speed * fix code SUPPORT_SET_SPEED --- .../components/freedompro/__init__.py | 2 +- homeassistant/components/freedompro/fan.py | 124 ++++++++++++++ tests/components/freedompro/test_fan.py | 159 ++++++++++++++++++ 3 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/freedompro/fan.py create mode 100644 tests/components/freedompro/test_fan.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 96045947814..650e479d027 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "cover", "light", "lock", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "lock", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py new file mode 100644 index 00000000000..55955042804 --- /dev/null +++ b/homeassistant/components/freedompro/fan.py @@ -0,0 +1,124 @@ +"""Support for Freedompro fan.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro fan.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + FreedomproFan(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] == "fan" + ) + + +class FreedomproFan(CoordinatorEntity, FanEntity): + """Representation of an Freedompro fan.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro fan.""" + super().__init__(coordinator) + self._session = aiohttp_client.async_get_clientsession(hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": device["type"], + "manufacturer": "Freedompro", + } + self._attr_is_on = False + self._attr_percentage = 0 + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._attr_is_on + + @property + def percentage(self): + """Return the current speed percentage.""" + return self._attr_percentage + + @property + def supported_features(self): + """Flag supported features.""" + if "rotationSpeed" in self._characteristics: + return SUPPORT_SET_SPEED + return 0 + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + self._attr_is_on = state["on"] + if "rotationSpeed" in state: + self._attr_percentage = state["rotationSpeed"] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_turn_on( + self, speed=None, percentage=None, preset_mode=None, **kwargs + ): + """Async function to turn on the fan.""" + payload = {"on": True} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Async function to turn off the fan.""" + payload = {"on": False} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_set_percentage(self, percentage: int): + """Set the speed percentage of the fan.""" + rotation_speed = {"rotationSpeed": percentage} + payload = json.dumps(rotation_speed) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py new file mode 100644 index 00000000000..6bf4bbe1e04 --- /dev/null +++ b/tests/components/freedompro/test_fan.py @@ -0,0 +1,159 @@ +"""Tests for the Freedompro fan.""" +from datetime import timedelta +from unittest.mock import ANY, patch + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + +uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS" + + +async def test_fan_get_state(hass, init_integration): + """Test states of the fan.""" + init_integration + registry = er.async_get(hass) + registry_device = dr.async_get(hass) + + device = registry_device.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == "bedroom" + assert device.model == "fan" + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_PERCENTAGE] == 0 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + state_response["state"]["on"] = True + state_response["state"]["rotationSpeed"] = 50 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + + +async def test_fan_set_off(hass, init_integration): + """Test turn off the fan.""" + init_integration + registry = er.async_get(hass) + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.fan.put_state") as mock_put_state: + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": false}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_ON + + +async def test_fan_set_on(hass, init_integration): + """Test turn on the fan.""" + init_integration + registry = er.async_get(hass) + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.fan.put_state") as mock_put_state: + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": true}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_ON + + +async def test_fan_set_percent(hass, init_integration): + """Test turn on the fan.""" + init_integration + registry = er.async_get(hass) + + entity_id = "fan.bedroom" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes.get("friendly_name") == "bedroom" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch("homeassistant.components.freedompro.fan.put_state") as mock_put_state: + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: [entity_id], ATTR_PERCENTAGE: 40}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"rotationSpeed": 40}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_ON From 9d79c4f617930e1b9d39aed37e4003a619e3c807 Mon Sep 17 00:00:00 2001 From: p4p3r <20265881+p4p3r@users.noreply.github.com> Date: Fri, 16 Jul 2021 15:48:35 +0200 Subject: [PATCH 325/818] Add On/Off as target values for zwave_js cover stop action (#52881) * Add On/Off as target values for stop cover Certain ZWave Cover devices use On/Off instead of the more common Open/Close and Up/Down targets for movement. Adding On/Off to the targets used to stop the cover during movement. Fixes issue #51963 * Add test for updated zwave_js stop cover logic --- homeassistant/components/zwave_js/cover.py | 23 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_cover.py | 210 ++++++++ .../cover_aeotec_nano_shutter_state.json | 498 ++++++++++++++++++ 4 files changed, 739 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/zwave_js/cover_aeotec_nano_shutter_state.json diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 71033277388..ec38fb1f3af 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -130,12 +130,23 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - target_value = self.get_zwave_value("Open") or self.get_zwave_value("Up") - if target_value: - await self.info.node.async_set_value(target_value, False) - target_value = self.get_zwave_value("Close") or self.get_zwave_value("Down") - if target_value: - await self.info.node.async_set_value(target_value, False) + open_value = ( + self.get_zwave_value("Open") + or self.get_zwave_value("Up") + or self.get_zwave_value("On") + ) + if open_value: + # Stop the cover if it's opening + await self.info.node.async_set_value(open_value, False) + + close_value = ( + self.get_zwave_value("Close") + or self.get_zwave_value("Down") + or self.get_zwave_value("Off") + ) + if close_value: + # Stop the cover if it's closing + await self.info.node.async_set_value(close_value, False) class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 02d5b10cbba..bfacd36301d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -350,6 +350,12 @@ def qubino_shutter_state_fixture(): return json.loads(load_fixture("zwave_js/cover_qubino_shutter_state.json")) +@pytest.fixture(name="aeotec_nano_shutter_state", scope="session") +def aeotec_nano_shutter_state_fixture(): + """Load the Aeotec Nano Shutter node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_aeotec_nano_shutter_state.json")) + + @pytest.fixture(name="aeon_smart_switch_6_state", scope="session") def aeon_smart_switch_6_state_fixture(): """Load the AEON Labs (ZW096) Smart Switch 6 node state fixture data.""" @@ -715,6 +721,14 @@ def qubino_shutter_cover_fixture(client, qubino_shutter_state): return node +@pytest.fixture(name="aeotec_nano_shutter") +def aeotec_nano_shutter_cover_fixture(client, aeotec_nano_shutter_state): + """Mock a Aeotec Nano Shutter node.""" + node = Node(client, copy.deepcopy(aeotec_nano_shutter_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="aeon_smart_switch_6") def aeon_smart_switch_6_fixture(client, aeon_smart_switch_6_state): """Mock an AEON Labs (ZW096) Smart Switch 6 node.""" diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 9d7a16ac8cf..70ce2337abf 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -24,6 +24,7 @@ WINDOW_COVER_ENTITY = "cover.zws_12" GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" BLIND_COVER_ENTITY = "cover.window_blind_controller" SHUTTER_COVER_ENTITY = "cover.flush_shutter_dc" +AEOTEC_SHUTTER_COVER_ENTITY = "cover.nano_shutter_v_3" async def test_window_cover(hass, client, chain_actuator_zws12, integration): @@ -306,6 +307,215 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert state.state == "closed" +async def test_aeotec_nano_shutter_cover( + hass, client, aeotec_nano_shutter, integration +): + """Test movement of an Aeotec Nano Shutter cover entity. Useful to make sure the stop command logic is handled properly.""" + node = aeotec_nano_shutter + state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY) + + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_WINDOW + + assert state.state == "closed" + assert state.attributes[ATTR_CURRENT_POSITION] == 0 + + # Test opening + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 3 + assert args["valueId"] == { + "ccVersion": 4, + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "value": 0, + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "valueChangeOptions": ["transitionDuration"], + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] + + client.async_send_command.reset_mock() + # Test stop after opening + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + open_args = client.async_send_command.call_args_list[0][0][0] + assert open_args["command"] == "node.set_value" + assert open_args["nodeId"] == 3 + assert open_args["valueId"] == { + "ccVersion": 4, + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "On", + "propertyName": "On", + "value": False, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (On)", + "ccSpecific": {"switchType": 1}, + }, + } + assert not open_args["value"] + + close_args = client.async_send_command.call_args_list[1][0][0] + assert close_args["command"] == "node.set_value" + assert close_args["nodeId"] == 3 + assert close_args["valueId"] == { + "ccVersion": 4, + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Off", + "propertyName": "Off", + "value": False, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Off)", + "ccSpecific": {"switchType": 1}, + }, + } + assert not close_args["value"] + + # Test position update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + client.async_send_command.reset_mock() + + state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY) + assert state.state == "open" + + # Test closing + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 3 + assert args["valueId"] == { + "ccVersion": 4, + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "value": 0, + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + "label": "Target value", + }, + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + # Test stop after closing + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + open_args = client.async_send_command.call_args_list[0][0][0] + assert open_args["command"] == "node.set_value" + assert open_args["nodeId"] == 3 + assert open_args["valueId"] == { + "ccVersion": 4, + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "On", + "propertyName": "On", + "value": False, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (On)", + "ccSpecific": {"switchType": 1}, + }, + } + assert not open_args["value"] + + close_args = client.async_send_command.call_args_list[1][0][0] + assert close_args["command"] == "node.set_value" + assert close_args["nodeId"] == 3 + assert close_args["valueId"] == { + "ccVersion": 4, + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Off", + "propertyName": "Off", + "value": False, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Off)", + "ccSpecific": {"switchType": 1}, + }, + } + assert not close_args["value"] + + async def test_blind_cover(hass, client, iblinds_v2, integration): """Test a blind cover entity.""" state = hass.states.get(BLIND_COVER_ENTITY) diff --git a/tests/fixtures/zwave_js/cover_aeotec_nano_shutter_state.json b/tests/fixtures/zwave_js/cover_aeotec_nano_shutter_state.json new file mode 100644 index 00000000000..b5373f38ec4 --- /dev/null +++ b/tests/fixtures/zwave_js/cover_aeotec_nano_shutter_state.json @@ -0,0 +1,498 @@ +{ + "nodeId": 3, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": true, + "manufacturerId": 881, + "productId": 141, + "productType": 3, + "firmwareVersion": "3.1", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0371/zw141.json", + "manufacturer": "Aeotec Ltd.", + "manufacturerId": 881, + "label": "ZW141", + "description": "Nano Shutter V.3", + "devices": [ + { + "productType": 3, + "productId": 141 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "3.5.2 Classic inclusion Learn Mode\n1. Set your Z-Wave Controller into its 'Add Device' mode in order to add the product into your Z-Wave system. Refer to the Controller's manual if you are unsure of how to perform this step.\n2. Make sure the product is powered. If not, plug it into a wall socket and power on; its LED will be breathing blue light all the time.\n3. Click Action Button once, it will quickly flash blue light for 30 seconds until it is added into the network. It will become constantly bright yellow light after being assigned a NodeID.\n4. If your Z-Wave Controller supports S2 encryption, enter the first 5 digits of DSK into your Controller's interface if/when requested. The DSK is printed on its housing.\n5. If Adding fails, it will bright red light for 2s and then become breathing blue light; repeat steps 1 to 4. Contact us for further support if needed.\n6. If Adding succeeds, it will bright blue light for 2s and then turn to Load Indicator Mode. Now, this product is a part of your Z-Wave home control system. You can configure it and its automations via your Z-Wave system; please refer to your software's user guide for precise instructions", + "exclusion": "3.6 How to Remove the device from Z-Wave network\n1. Set your Z-Wave Controller into its 'Remove Device' mode in order to remove the product from your Z-Wave system.Refer to the Controller's manual if you are unsure of how to perform this step.\n2. Click Action Button/S1/S2(external switch need to be identified first) 6 times will enter exclusion mode.\n3. If Removing fails, it will bright red light for 2s then turn back to Regular Light Mode, repeat steps 1-2. Contact us for further support if needed.\n4. If Removing succeeds, it will become breathing blue light. Now, it is removed from Z-Wave network successfully", + "reset": "3.7 How to Factory Reset\nManually, press and hold the Action Button for at least 20s and then release. The LED indicator will become breathing blue light, which indicates the reset operation is successful. Otherwise, please try again. Contact us for further support if needed.\nNote:\n1. This procedure should only be used when the primary controller is missing or inoperable.\n2. Factory Reset will:\na) Remove the product from Z-Wave network;\nb) Delete the Association setting;", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/3693/Nano%20Shutter%20-%20Product%20Manual.pdf" + }, + "isEmbedded": true + }, + "label": "ZW141", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 3, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "On", + "propertyName": "On", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (On)", + "ccSpecific": { + "switchType": 1 + } + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Off", + "propertyName": "Off", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Off)", + "ccSpecific": { + "switchType": 1 + } + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 1, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms.", + "label": "Send held down notifications at a slow rate" + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Scene 001", + "min": 0, + "max": 255, + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "002", + "propertyName": "scene", + "propertyKeyName": "002", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Scene 002", + "min": 0, + "max": 255, + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x" + } + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 881 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 141 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.4" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "3.1" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": true + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1" +} From 669883d41661cfd593f63c3d72fc5ce732b4c5a7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 16 Jul 2021 18:46:23 +0200 Subject: [PATCH 326/818] Update Plugwise Config Flow (#47563) Co-authored-by: Tom Scholten --- .../components/plugwise/config_flow.py | 96 ++++++--- homeassistant/components/plugwise/const.py | 62 +++--- tests/components/plugwise/test_config_flow.py | 185 +++++++++++++----- 3 files changed, 244 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index d19c3b49920..abecda7f728 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -19,10 +19,41 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType -from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, ZEROCONF_MAP +from .const import ( + API, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_USERNAME, + DOMAIN, + FLOW_NET, + FLOW_SMILE, + FLOW_STRETCH, + FLOW_TYPE, + FLOW_USB, + PW_TYPE, + SMILE, + STRETCH, + STRETCH_USERNAME, + ZEROCONF_MAP, +) _LOGGER = logging.getLogger(__name__) +CONF_MANUAL_PATH = "Enter Manually" + +CONNECTION_SCHEMA = vol.Schema( + { + vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In( + { + FLOW_NET: f"Network: {SMILE} / {STRETCH}", + FLOW_USB: "USB: To be added later", + } + ), + }, +) + +# PLACEHOLDER USB connection validation + def _base_gw_schema(discovery_info): """Generate base schema for gateways.""" @@ -31,22 +62,18 @@ def _base_gw_schema(discovery_info): if not discovery_info: base_gw_schema[vol.Required(CONF_HOST)] = str base_gw_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int + base_gw_schema[vol.Required(CONF_USERNAME, default=SMILE)] = vol.In( + {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH} + ) - base_gw_schema.update( - { - vol.Required( - CONF_USERNAME, default="smile", description={"suggested_value": "smile"} - ): str, - vol.Required(CONF_PASSWORD): str, - } - ) + base_gw_schema.update({vol.Required(CONF_PASSWORD): str}) return vol.Schema(base_gw_schema) async def validate_gw_input(hass: core.HomeAssistant, data): """ - Validate whether the user input allows us to connect to the gateray. + Validate whether the user input allows us to connect to the gateway. Data has the keys from _base_gw_schema() with values provided by the user. """ @@ -71,9 +98,6 @@ async def validate_gw_input(hass: core.HomeAssistant, data): return api -# PLACEHOLDER USB connection validation - - class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Plugwise Smile.""" @@ -86,32 +110,43 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Prepare configuration for a discovered Plugwise Smile.""" self.discovery_info = discovery_info + self.discovery_info[CONF_USERNAME] = DEFAULT_USERNAME _properties = self.discovery_info.get("properties") + # unique_id is needed here, to be able to determine whether the discovered device is known, or not. unique_id = self.discovery_info.get("hostname").split(".")[0] await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() + if DEFAULT_USERNAME not in unique_id: + self.discovery_info[CONF_USERNAME] = STRETCH_USERNAME _product = _properties.get("product", None) _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT), + CONF_HOST: self.discovery_info[CONF_HOST], CONF_NAME: _name, + CONF_PORT: self.discovery_info[CONF_PORT], + CONF_USERNAME: self.discovery_info[CONF_USERNAME], } - return await self.async_step_user() + return await self.async_step_user_gateway() + + # PLACEHOLDER USB step_user async def async_step_user_gateway(self, user_input=None): - """Handle the initial step for gateways.""" + """Handle the initial step when using network/gateway setups.""" + api = None errors = {} if user_input is not None: + user_input.pop(FLOW_TYPE, None) if self.discovery_info: user_input[CONF_HOST] = self.discovery_info[CONF_HOST] - user_input[CONF_PORT] = self.discovery_info.get(CONF_PORT, DEFAULT_PORT) + user_input[CONF_PORT] = self.discovery_info[CONF_PORT] + user_input[CONF_USERNAME] = self.discovery_info[CONF_USERNAME] self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) @@ -125,26 +160,36 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors[CONF_BASE] = "unknown" + if not errors: await self.async_set_unique_id( api.smile_hostname or api.gateway_id, raise_on_progress=False ) self._abort_if_unique_id_configured() + user_input[PW_TYPE] = API return self.async_create_entry(title=api.smile_name, data=user_input) return self.async_show_form( step_id="user_gateway", data_schema=_base_gw_schema(self.discovery_info), - errors=errors or {}, + errors=errors, ) - # PLACEHOLDER USB async_step_user_usb and async_step_user_usb_manual_paht - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - # PLACEHOLDER USB vs Gateway Logic - return await self.async_step_user_gateway() + """Handle the initial step when using network/gateway setups.""" + errors = {} + if user_input is not None: + if user_input[FLOW_TYPE] == FLOW_NET: + return await self.async_step_user_gateway() + + # PLACEHOLDER for USB_FLOW + + return self.async_show_form( + step_id="user", + data_schema=CONNECTION_SCHEMA, + errors=errors, + ) @staticmethod @callback @@ -165,8 +210,9 @@ class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - api = self.hass.data[DOMAIN][self.config_entry.entry_id]["api"] + api = self.hass.data[DOMAIN][self.config_entry.entry_id][API] interval = DEFAULT_SCAN_INTERVAL[api.smile_type] + data = { vol.Optional( CONF_SCAN_INTERVAL, diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index fb8911d6fc7..a7afbd9e197 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -1,10 +1,33 @@ -"""Constant for Plugwise component.""" -DOMAIN = "plugwise" +"""Constants for Plugwise component.""" -SENSOR_PLATFORMS = ["sensor", "switch"] -PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"] -PW_TYPE = "plugwise_type" +API = "api" +ATTR_ILLUMINANCE = "illuminance" +COORDINATOR = "coordinator" +DEVICE_STATE = "device_state" +DOMAIN = "plugwise" +FLOW_NET = "flow_network" +FLOW_SMILE = "smile (Adam/Anna/P1)" +FLOW_STRETCH = "stretch (Stretch)" +FLOW_TYPE = "flow_type" +FLOW_USB = "flow_usb" GATEWAY = "gateway" +PW_TYPE = "plugwise_type" +SCHEDULE_OFF = "false" +SCHEDULE_ON = "true" +SMILE = "smile" +STRETCH = "stretch" +STRETCH_USERNAME = "stretch" +UNDO_UPDATE_LISTENER = "undo_update_listener" +UNIT_LUMEN = "lm" + +PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"] +SENSOR_PLATFORMS = ["sensor", "switch"] +ZEROCONF_MAP = { + "smile": "P1", + "smile_thermo": "Anna", + "smile_open_therm": "Adam", + "stretch": "Stretch", +} # Sensor mapping SENSOR_MAP_DEVICE_CLASS = 2 @@ -13,13 +36,17 @@ SENSOR_MAP_MODEL = 0 SENSOR_MAP_UOM = 1 # Default directives -DEFAULT_MIN_TEMP = 4 DEFAULT_MAX_TEMP = 30 +DEFAULT_MIN_TEMP = 4 DEFAULT_NAME = "Smile" DEFAULT_PORT = 80 -DEFAULT_USERNAME = "smile" -DEFAULT_SCAN_INTERVAL = {"power": 10, "stretch": 60, "thermostat": 60} +DEFAULT_SCAN_INTERVAL = { + "power": 10, + "stretch": 60, + "thermostat": 60, +} DEFAULT_TIMEOUT = 60 +DEFAULT_USERNAME = "smile" # Configuration directives CONF_GAS = "gas" @@ -28,15 +55,7 @@ CONF_MIN_TEMP = "min_temp" CONF_POWER = "power" CONF_THERMOSTAT = "thermostat" -ATTR_ILLUMINANCE = "illuminance" - -UNIT_LUMEN = "lm" - -DEVICE_STATE = "device_state" - -SCHEDULE_OFF = "false" -SCHEDULE_ON = "true" - +# Icons COOL_ICON = "mdi:snowflake" FLAME_ICON = "mdi:fire" FLOW_OFF_ICON = "mdi:water-pump-off" @@ -45,12 +64,3 @@ IDLE_ICON = "mdi:circle-off-outline" SWITCH_ICON = "mdi:electric-switch" NO_NOTIFICATION_ICON = "mdi:mailbox-outline" NOTIFICATION_ICON = "mdi:mailbox-up-outline" - -COORDINATOR = "coordinator" -UNDO_UPDATE_LISTENER = "undo_update_listener" -ZEROCONF_MAP = { - "smile": "P1", - "smile_thermo": "Anna", - "smile_open_therm": "Adam", - "stretch": "Stretch", -} diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 1697a43127e..04c42e0ba83 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Plugwise config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from plugwise.exceptions import ( ConnectionFailedError, @@ -8,11 +8,15 @@ from plugwise.exceptions import ( ) import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import setup from homeassistant.components.plugwise.const import ( + API, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, + FLOW_NET, + FLOW_TYPE, + PW_TYPE, ) from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( @@ -21,13 +25,16 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, + CONF_SOURCE, CONF_USERNAME, ) +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from tests.common import MockConfigEntry TEST_HOST = "1.1.1.1" TEST_HOSTNAME = "smileabcdef" +TEST_HOSTNAME2 = "stretchabc" TEST_PASSWORD = "test_password" TEST_PORT = 81 TEST_USERNAME = "smile" @@ -44,6 +51,17 @@ TEST_DISCOVERY = { "hostname": f"{TEST_HOSTNAME}.local.", }, } +TEST_DISCOVERY2 = { + "host": TEST_HOST, + "port": DEFAULT_PORT, + "hostname": f"{TEST_HOSTNAME2}.local.", + "server": f"{TEST_HOSTNAME2}.local.", + "properties": { + "product": "stretch", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME2}.local.", + }, +} @pytest.fixture(name="mock_smile") @@ -52,20 +70,38 @@ def mock_smile(): with patch( "homeassistant.components.plugwise.config_flow.Smile", ) as smile_mock: - smile_mock.PlugwiseError = PlugwiseException + smile_mock.PlugwiseException = PlugwiseException smile_mock.InvalidAuthentication = InvalidAuthentication smile_mock.ConnectionFailedError = ConnectionFailedError smile_mock.return_value.connect.return_value = True yield smile_mock.return_value +async def test_form_flow_gateway(hass): + """Test we get the form for Plugwise Gateway product type.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={FLOW_TYPE: FLOW_NET} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "user_gateway" + + async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -77,17 +113,18 @@ async def test_form(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, + user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, CONF_USERNAME: TEST_USERNAME, + PW_TYPE: API, } assert len(mock_setup_entry.mock_calls) == 1 @@ -98,10 +135,10 @@ async def test_zeroconf_form(hass): await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_ZEROCONF}, + context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -113,17 +150,55 @@ async def test_zeroconf_form(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: TEST_PASSWORD}, + user_input={CONF_PASSWORD: TEST_PASSWORD}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, CONF_USERNAME: TEST_USERNAME, + PW_TYPE: API, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_stretch_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY2, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.plugwise.config_flow.Smile.connect", + return_value=True, + ), patch( + "homeassistant.components.plugwise.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: DEFAULT_PORT, + CONF_USERNAME: TEST_USERNAME2, + PW_TYPE: API, } assert len(mock_setup_entry.mock_calls) == 1 @@ -131,58 +206,68 @@ async def test_zeroconf_form(hass): async def test_form_username(hass): """Test we get the username data back.""" + await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( + "homeassistant.components.plugwise.config_flow.Smile", + ) as smile_mock, patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True, ) as mock_setup_entry: + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.gateway_id = "abcdefgh12345678" + smile_mock.return_value.smile_hostname = TEST_HOST + smile_mock.return_value.smile_name = "Adam" + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { + user_input={ CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_USERNAME: TEST_USERNAME2, }, ) - await hass.async_block_till_done() + await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_PORT: DEFAULT_PORT, - CONF_USERNAME: TEST_USERNAME2, - } + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: DEFAULT_PORT, + CONF_USERNAME: TEST_USERNAME2, + PW_TYPE: API, + } assert len(mock_setup_entry.mock_calls) == 1 result3 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_ZEROCONF}, + context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["errors"] == {} + assert result3["type"] == RESULT_TYPE_FORM with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( + "homeassistant.components.plugwise.config_flow.Smile", + ) as smile_mock, patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True, ) as mock_setup_entry: + smile_mock.return_value.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.gateway_id = "abcdefgh12345678" + smile_mock.return_value.smile_hostname = TEST_HOST + smile_mock.return_value.smile_name = "Adam" + result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], - {CONF_PASSWORD: TEST_PASSWORD}, + user_input={CONF_PASSWORD: TEST_PASSWORD}, ) await hass.async_block_till_done() @@ -194,7 +279,7 @@ async def test_form_username(hass): async def test_form_invalid_auth(hass, mock_smile): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) mock_smile.connect.side_effect = InvalidAuthentication @@ -202,17 +287,17 @@ async def test_form_invalid_auth(hass, mock_smile): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, + user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} async def test_form_cannot_connect(hass, mock_smile): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) mock_smile.connect.side_effect = ConnectionFailedError @@ -220,17 +305,17 @@ async def test_form_cannot_connect(hass, mock_smile): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, + user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} async def test_form_cannot_connect_port(hass, mock_smile): """Test we handle cannot connect to port error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) mock_smile.connect.side_effect = ConnectionFailedError @@ -238,17 +323,21 @@ async def test_form_cannot_connect_port(hass, mock_smile): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: TEST_PORT}, + user_input={ + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} async def test_form_other_problem(hass, mock_smile): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) mock_smile.connect.side_effect = TimeoutError @@ -256,10 +345,10 @@ async def test_form_other_problem(hass, mock_smile): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, + user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "unknown"} @@ -283,13 +372,13 @@ async def test_options_flow_power(hass, mock_smile) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 10} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: 10, } @@ -315,14 +404,14 @@ async def test_options_flow_thermo(hass, mock_smile) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 60} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: 60, } From b13119884c7ccb46ff493e9f75b4dfa7cfe5168d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 16 Jul 2021 20:01:00 +0200 Subject: [PATCH 327/818] Fix units for Fritz network sensors (#53026) --- homeassistant/components/fritz/sensor.py | 48 ++++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 5801bc752fe..5820be138b4 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -42,22 +42,22 @@ def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: return status.external_ip # type: ignore[no-any-return] -def _retrieve_kib_s_sent_state(status: FritzStatus, last_value: str) -> float: +def _retrieve_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload transmission rate.""" - return round(status.transmission_rate[0] * 8 / 1024, 1) # type: ignore[no-any-return] + return round(status.transmission_rate[0] / 1024, 1) # type: ignore[no-any-return] -def _retrieve_kib_s_received_state(status: FritzStatus, last_value: str) -> float: +def _retrieve_kb_s_received_state(status: FritzStatus, last_value: str) -> float: """Return download transmission rate.""" - return round(status.transmission_rate[1] * 8 / 1024, 1) # type: ignore[no-any-return] + return round(status.transmission_rate[1] / 1024, 1) # type: ignore[no-any-return] -def _retrieve_max_kib_s_sent_state(status: FritzStatus, last_value: str) -> float: +def _retrieve_max_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload max transmission rate.""" return round(status.max_bit_rate[0] / 1024, 1) # type: ignore[no-any-return] -def _retrieve_max_kib_s_received_state(status: FritzStatus, last_value: str) -> float: +def _retrieve_max_kb_s_received_state(status: FritzStatus, last_value: str) -> float: """Return download max transmission rate.""" return round(status.max_bit_rate[1] / 1024, 1) # type: ignore[no-any-return] @@ -94,39 +94,39 @@ SENSOR_DATA = { device_class=DEVICE_CLASS_TIMESTAMP, state_provider=_retrieve_uptime_state, ), - "kib_s_sent": SensorData( - name="KiB/s sent", + "kb_s_sent": SensorData( + name="kB/s sent", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement="KiB/s", + unit_of_measurement="kB/s", icon="mdi:upload", - state_provider=_retrieve_kib_s_sent_state, + state_provider=_retrieve_kb_s_sent_state, ), - "kib_s_received": SensorData( - name="KiB/s received", + "kb_s_received": SensorData( + name="kB/s received", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement="KiB/s", + unit_of_measurement="kB/s", icon="mdi:download", - state_provider=_retrieve_kib_s_received_state, + state_provider=_retrieve_kb_s_received_state, ), - "max_kib_s_sent": SensorData( - name="Max KiB/s sent", - unit_of_measurement="KiB/s", + "max_kb_s_sent": SensorData( + name="Max kb/s sent", + unit_of_measurement="kb/s", icon="mdi:upload", - state_provider=_retrieve_max_kib_s_sent_state, + state_provider=_retrieve_max_kb_s_sent_state, ), - "max_kib_s_received": SensorData( - name="Max KiB/s received", - unit_of_measurement="KiB/s", + "max_kb_s_received": SensorData( + name="Max kb/s received", + unit_of_measurement="kb/s", icon="mdi:download", - state_provider=_retrieve_max_kib_s_received_state, + state_provider=_retrieve_max_kb_s_received_state, ), - "mb_sent": SensorData( + "gb_sent": SensorData( name="GB sent", unit_of_measurement="GB", icon="mdi:upload", state_provider=_retrieve_gb_sent_state, ), - "mb_received": SensorData( + "gb_received": SensorData( name="GB received", unit_of_measurement="GB", icon="mdi:download", From 4fceac00b1868fad46f96ea6cfcd89d810a695e6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 16 Jul 2021 17:06:18 -0400 Subject: [PATCH 328/818] Use entity class attributes for Bond (#53055) --- homeassistant/components/bond/cover.py | 18 ++---- homeassistant/components/bond/entity.py | 53 +++++---------- homeassistant/components/bond/light.py | 85 ++++--------------------- homeassistant/components/bond/switch.py | 17 +---- 4 files changed, 37 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index ca8432531e5..3a2777b09e8 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -38,27 +38,19 @@ async def async_setup_entry( class BondCover(BondEntity, CoverEntity): """Representation of a Bond cover.""" + _attr_device_class = DEVICE_CLASS_SHADE + def __init__( self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions ) -> None: """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) - self._closed: bool | None = None - def _apply_state(self, state: dict) -> None: cover_open = state.get("open") - self._closed = True if cover_open == 0 else False if cover_open == 1 else None - - @property - def device_class(self) -> str | None: - """Get device class.""" - return DEVICE_CLASS_SHADE - - @property - def is_closed(self) -> bool | None: - """Return if the cover is closed or not.""" - return self._closed + self._attr_is_closed = ( + True if cover_open == 0 else False if cover_open == 1 else None + ) async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 59425af54d0..3063a3e4efa 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -25,6 +25,8 @@ _FALLBACK_SCAN_INTERVAL = timedelta(seconds=10) class BondEntity(Entity): """Generic Bond entity encapsulating common features of any Bond controlled device.""" + _attr_should_poll = False + def __init__( self, hub: BondHub, @@ -37,31 +39,17 @@ class BondEntity(Entity): self._device = device self._device_id = device.device_id self._sub_device = sub_device - self._available = True + self._attr_available = True self._bpup_subs = bpup_subs self._update_lock: Lock | None = None self._initialized = False - - @property - def unique_id(self) -> str | None: - """Get unique ID for the entity.""" - hub_id = self._hub.bond_id - device_id = self._device_id - sub_device_id: str = f"_{self._sub_device}" if self._sub_device else "" - return f"{hub_id}_{device_id}{sub_device_id}" - - @property - def name(self) -> str | None: - """Get entity name.""" - if self._sub_device: - sub_device_name = self._sub_device.replace("_", " ").title() - return f"{self._device.name} {sub_device_name}" - return self._device.name - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False + sub_device_id: str = f"_{sub_device}" if sub_device else "" + self._attr_unique_id = f"{hub.bond_id}_{device.device_id}{sub_device_id}" + if sub_device: + sub_device_name = sub_device.replace("_", " ").title() + self._attr_name = f"{device.name} {sub_device_name}" + else: + self._attr_name = device.name @property def device_info(self) -> DeviceInfo: @@ -93,16 +81,6 @@ class BondEntity(Entity): return device_info - @property - def assumed_state(self) -> bool: - """Let HA know this entity relies on an assumed state tracked by Bond.""" - return self._hub.is_bridge and not self._device.trust_state - - @property - def available(self) -> bool: - """Report availability of this entity based on last API call results.""" - return self._available - async def async_update(self) -> None: """Fetch assumed state of the cover from the hub using API.""" await self._async_update_from_api() @@ -113,7 +91,7 @@ class BondEntity(Entity): self.hass.is_stopping or self._bpup_subs.alive and self._initialized - and self._available + and self.available ): return @@ -135,13 +113,14 @@ class BondEntity(Entity): try: state: dict = await self._hub.bond.device_state(self._device_id) except (ClientError, AsyncIOTimeoutError, OSError) as error: - if self._available: + if self.available: _LOGGER.warning( "Entity %s has become unavailable", self.entity_id, exc_info=error ) - self._available = False + self._attr_available = False else: self._async_state_callback(state) + self._attr_assumed_state = self._hub.is_bridge and not self._device.trust_state @abstractmethod def _apply_state(self, state: dict) -> None: @@ -151,9 +130,9 @@ class BondEntity(Entity): def _async_state_callback(self, state: dict) -> None: """Process a state change.""" self._initialized = True - if not self._available: + if not self.available: _LOGGER.info("Entity %s has come back", self.entity_id) - self._available = True + self._attr_available = True _LOGGER.debug( "Device state for %s (%s) is:\n%s", self.name, self.entity_id, state ) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index ca9cbf45a7a..31eceda6c41 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -81,26 +81,7 @@ async def async_setup_entry( class BondBaseLight(BondEntity, LightEntity): """Representation of a Bond light.""" - def __init__( - self, - hub: BondHub, - device: BondDevice, - bpup_subs: BPUPSubscriptions, - sub_device: str | None = None, - ) -> None: - """Create HA entity representing Bond light.""" - super().__init__(hub, device, bpup_subs, sub_device) - self._light: int | None = None - - @property - def is_on(self) -> bool: - """Return if light is currently on.""" - return self._light == 1 - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return 0 + _attr_supported_features = 0 class BondLight(BondBaseLight, BondEntity, LightEntity): @@ -115,26 +96,13 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): ) -> None: """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) - self._brightness: int | None = None + if device.supports_set_brightness(): + self._attr_supported_features = SUPPORT_BRIGHTNESS def _apply_state(self, state: dict) -> None: - self._light = state.get("light") - self._brightness = state.get("brightness") - - @property - def supported_features(self) -> int: - """Flag supported features.""" - if self._device.supports_set_brightness(): - return SUPPORT_BRIGHTNESS - return 0 - - @property - def brightness(self) -> int | None: - """Return the brightness of this light between 1..255.""" - brightness_value = ( - round(self._brightness * 255 / 100) if self._brightness else None - ) - return brightness_value + self._attr_is_on = state.get("light") == 1 + brightness = state.get("brightness") + self._attr_brightness = round(brightness * 255 / 100) if brightness else None async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" @@ -156,7 +124,7 @@ class BondDownLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" def _apply_state(self, state: dict) -> None: - self._light = state.get("down_light") and state.get("light") + self._attr_is_on = bool(state.get("down_light") and state.get("light")) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" @@ -175,7 +143,7 @@ class BondUpLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" def _apply_state(self, state: dict) -> None: - self._light = state.get("up_light") and state.get("light") + self._attr_is_on = bool(state.get("up_light") and state.get("light")) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" @@ -193,29 +161,14 @@ class BondUpLight(BondBaseLight, BondEntity, LightEntity): class BondFireplace(BondEntity, LightEntity): """Representation of a Bond-controlled fireplace.""" - def __init__( - self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions - ) -> None: - """Create HA entity representing Bond fireplace.""" - super().__init__(hub, device, bpup_subs) - - self._power: bool | None = None - # Bond flame level, 0-100 - self._flame: int | None = None + _attr_supported_features = SUPPORT_BRIGHTNESS def _apply_state(self, state: dict) -> None: - self._power = state.get("power") - self._flame = state.get("flame") - - @property - def supported_features(self) -> int: - """Flag brightness as supported feature to represent flame level.""" - return SUPPORT_BRIGHTNESS - - @property - def is_on(self) -> bool: - """Return True if power is on.""" - return self._power == 1 + power = state.get("power") + flame = state.get("flame") + self._attr_is_on = power == 1 + self._attr_brightness = round(flame * 255 / 100) if flame else None + self._attr_icon = "mdi:fireplace" if power == 1 else "mdi:fireplace-off" async def async_turn_on(self, **kwargs: Any) -> None: """Turn the fireplace on.""" @@ -233,13 +186,3 @@ class BondFireplace(BondEntity, LightEntity): _LOGGER.debug("Fireplace async_turn_off called with: %s", kwargs) await self._hub.bond.action(self._device.device_id, Action.turn_off()) - - @property - def brightness(self) -> int | None: - """Return the flame of this fireplace converted to HA brightness between 0..255.""" - return round(self._flame * 255 / 100) if self._flame else None - - @property - def icon(self) -> str | None: - """Show fireplace icon for the entity.""" - return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off" diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index 0f323b1609b..0bb58946f0f 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import BPUP_SUBS, DOMAIN, HUB from .entity import BondEntity -from .utils import BondDevice, BondHub +from .utils import BondHub async def async_setup_entry( @@ -38,21 +38,8 @@ async def async_setup_entry( class BondSwitch(BondEntity, SwitchEntity): """Representation of a Bond generic device.""" - def __init__( - self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions - ) -> None: - """Create HA entity representing Bond generic device (switch).""" - super().__init__(hub, device, bpup_subs) - - self._power: bool | None = None - def _apply_state(self, state: dict) -> None: - self._power = state.get("power") - - @property - def is_on(self) -> bool: - """Return True if power is on.""" - return self._power == 1 + self._attr_is_on = state.get("power") == 1 async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" From 0277a645f1f4617e5ae24c5b573f858596dbc86d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 16 Jul 2021 16:12:01 -0500 Subject: [PATCH 329/818] Log source of discovery in Sonos (#53101) --- homeassistant/components/sonos/__init__.py | 8 +++++--- homeassistant/components/sonos/config_flow.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 3d810c7e1a3..73e9ab0caf0 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -261,11 +261,13 @@ class SonosDiscoveryManager: if uid.startswith("uuid:"): uid = uid[5:] self.async_discovered_player( - info, discovered_ip, uid, boot_seqnum, info.get("modelName") + "SSDP", info, discovered_ip, uid, boot_seqnum, info.get("modelName") ) @callback - def async_discovered_player(self, info, discovered_ip, uid, boot_seqnum, model): + def async_discovered_player( + self, source, info, discovered_ip, uid, boot_seqnum, model + ): """Handle discovery via ssdp or zeroconf.""" if model in DISCOVERY_IGNORED_MODELS: _LOGGER.debug("Ignoring device: %s", info) @@ -274,7 +276,7 @@ class SonosDiscoveryManager: boot_seqnum = int(boot_seqnum) self.data.boot_counts.setdefault(uid, boot_seqnum) if uid not in self.data.discovery_known: - _LOGGER.debug("New discovery uid=%s: %s", uid, info) + _LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info) self.data.discovery_known.add(uid) asyncio.create_task( self._async_create_discovered_player(uid, discovered_ip, boot_seqnum) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 1ba750c24be..9ee571471bd 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -50,7 +50,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): ) if discovery_manager := self.hass.data.get(DATA_SONOS_DISCOVERY_MANAGER): discovery_manager.async_discovered_player( - properties, host, uid, boot_seqnum, model + "Zeroconf", properties, host, uid, boot_seqnum, model ) return await self.async_step_discovery(discovery_info) From e6e1118dd4e87867b335b5938a18f43e10636238 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Jul 2021 00:09:24 +0000 Subject: [PATCH 330/818] [ci skip] Translation update --- .../alarm_control_panel/translations/es.json | 4 ++ .../cloudflare/translations/es.json | 5 +++ .../components/coinbase/translations/ca.json | 1 + .../components/coinbase/translations/es.json | 31 ++++++++++++++ .../components/coinbase/translations/it.json | 1 + .../components/dsmr/translations/es.json | 24 ++++++++++- .../forecast_solar/translations/es.json | 9 ++++ .../freedompro/translations/es.json | 10 +++++ .../huawei_lte/translations/es.json | 3 +- .../huawei_lte/translations/it.json | 5 ++- .../components/motioneye/translations/es.json | 10 +++++ .../nmap_tracker/translations/es.json | 29 +++++++++++++ .../components/onvif/translations/es.json | 6 +++ .../philips_js/translations/es.json | 9 ++++ .../components/sonos/translations/es.json | 1 + .../components/sonos/translations/it.json | 1 + .../components/zwave_js/translations/ca.json | 3 ++ .../components/zwave_js/translations/es.json | 42 +++++++++++++++++++ .../components/zwave_js/translations/it.json | 15 +++++++ 19 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/es.json create mode 100644 homeassistant/components/forecast_solar/translations/es.json create mode 100644 homeassistant/components/freedompro/translations/es.json create mode 100644 homeassistant/components/nmap_tracker/translations/es.json diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index ab4e4a20cce..a76c6bd5af9 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -4,6 +4,7 @@ "arm_away": "Armar {entity_name} exterior", "arm_home": "Armar {entity_name} modo casa", "arm_night": "Armar {entity_name} por la noche", + "arm_vacation": "Armar las vacaciones de {entity_name}", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} est\u00e1 armada ausente", "is_armed_home": "{entity_name} est\u00e1 armada en casa", "is_armed_night": "{entity_name} est\u00e1 armada noche", + "is_armed_vacation": "{entity_name} est\u00e1 armado de vacaciones", "is_disarmed": "{entity_name} est\u00e1 desarmada", "is_triggered": "{entity_name} est\u00e1 disparada" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} armada ausente", "armed_home": "{entity_name} armada en casa", "armed_night": "{entity_name} armada noche", + "armed_vacation": "Vacaciones armadas de {entity_name}", "disarmed": "{entity_name} desarmada", "triggered": "{entity_name} activado" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Armada personalizada", "armed_home": "Armada en casa", "armed_night": "Armada noche", + "armed_vacation": "Vacaciones armadas", "arming": "Armando", "disarmed": "Desarmada", "disarming": "Desarmando", diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index 7f9fdc15dfb..8ef8fd15dd4 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -11,6 +11,11 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "description": "Vuelva a autenticarse con su cuenta de Cloudflare." + } + }, "records": { "data": { "records": "Registros" diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json index 0b512498fcf..a7990703ef0 100644 --- a/homeassistant/components/coinbase/translations/ca.json +++ b/homeassistant/components/coinbase/translations/ca.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Saldos de cartera a informar.", + "exchange_base": "Moneda base per als sensors de tipus de canvi.", "exchange_rate_currencies": "Tipus de canvi a informar." }, "description": "Ajusta les opcions de Coinbase" diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json new file mode 100644 index 00000000000..d5fee8ed5b3 --- /dev/null +++ b/homeassistant/components/coinbase/translations/es.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "Secreto de la API", + "currencies": "Saldo de la cuenta Monedas", + "exchange_rates": "Tipos de cambio" + }, + "description": "Por favor, introduce los detalles de tu clave API tal y como te la ha proporcionado Coinbase.", + "title": "Detalles de la clave API de Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", + "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados." + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Saldos de la cartera para informar.", + "exchange_base": "Moneda base para sensores de tipo de cambio.", + "exchange_rate_currencies": "Tipos de cambio a informar." + }, + "description": "Ajustar las opciones de Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index b45f263ef37..af5d07805de 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Saldi del portafoglio da segnalare.", + "exchange_base": "Valuta di base per i sensori di tasso di cambio.", "exchange_rate_currencies": "Tassi di cambio da segnalare." }, "description": "Regolare le opzioni di Coinbase" diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index a85293e93a0..024d5a55a00 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -5,7 +5,29 @@ }, "step": { "one": "Vac\u00edo", - "other": "Vac\u00edo" + "other": "Vac\u00edo", + "setup_network": { + "data": { + "dsmr_version": "Seleccione la versi\u00f3n de DSMR" + }, + "title": "Seleccione la direcci\u00f3n de la conexi\u00f3n" + }, + "setup_serial": { + "data": { + "dsmr_version": "Seleccione la versi\u00f3n de DSMR", + "port": "Seleccione el dispositivo" + }, + "title": "Dispositivo" + }, + "setup_serial_manual_path": { + "title": "Ruta" + }, + "user": { + "data": { + "type": "Tipo de conexi\u00f3n" + }, + "title": "Seleccione el tipo de conexi\u00f3n" + } } }, "options": { diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json new file mode 100644 index 00000000000..41846c60ab6 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "description": "Estos valores permiten ajustar el resultado de Solar.Forecast. Consulte la documentaci\u00f3n si un campo no est\u00e1 claro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/es.json b/homeassistant/components/freedompro/translations/es.json new file mode 100644 index 00000000000..5325f4216f3 --- /dev/null +++ b/homeassistant/components/freedompro/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Ingrese la clave API obtenida de https://home.freedompro.eu", + "title": "Clave API de Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 00564d7282a..5d5e72e70c3 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -35,7 +35,8 @@ "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", "recipient": "Destinatarios de notificaciones por SMS", "track_new_devices": "Rastrea nuevos dispositivos", - "track_wired_clients": "Seguir clientes de red cableados" + "track_wired_clients": "Seguir clientes de red cableados", + "unauthenticated_mode": "Modo no autenticado (el cambio requiere recarga)" } } } diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index df2c6fd441b..ad8d4a82b08 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Nome utente" }, - "description": "Immettere i dettagli di accesso al dispositivo. La specifica di nome utente e password \u00e8 facoltativa, ma abilita il supporto per altre funzionalit\u00e0 di integrazione. D'altra parte, l'uso di una connessione autorizzata pu\u00f2 causare problemi di accesso all'interfaccia Web del dispositivo dall'esterno di Home Assistant mentre l'integrazione \u00e8 attiva e viceversa.", + "description": "Inserisci i dettagli di accesso al dispositivo.", "title": "Configura Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", "track_new_devices": "Traccia nuovi dispositivi", - "track_wired_clients": "Tieni traccia dei client di rete cablata" + "track_wired_clients": "Tieni traccia dei client di rete cablata", + "unauthenticated_mode": "Modalit\u00e0 non autenticata (la modifica richiede il ricaricamento)" } } } diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index d018c52515e..b24502caffd 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -25,5 +25,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Configure los webhooks de motionEye para informar eventos a Home Assistant", + "webhook_set_overwrite": "Sobrescribir webhooks no reconocidos" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json new file mode 100644 index 00000000000..1d0d2252a5a --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hosts": "Hosts no v\u00e1lidos" + }, + "step": { + "user": { + "data": { + "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", + "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", + "hosts": "Direcciones de red (separadas por comas) para escanear", + "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" + }, + "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Intervalo de exploraci\u00f3n", + "track_new_devices": "Seguimiento de nuevos dispositivos" + } + } + } + }, + "title": "Rastreador de Nmap" +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 5b52990dde5..8b44955efdc 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -18,6 +18,9 @@ }, "title": "Configurar la autenticaci\u00f3n" }, + "configure": { + "title": "Configurar dispositivo ONVIF" + }, "configure_profile": { "data": { "include": "Crear la entidad de la c\u00e1mara" @@ -40,6 +43,9 @@ "title": "Configurar el dispositivo ONVIF" }, "user": { + "data": { + "auto": "Buscar autom\u00e1ticamente" + }, "description": "Al hacer clic en Enviar, buscaremos en su red dispositivos ONVIF compatibles con el perfil S.\n\nAlgunos fabricantes han comenzado a desactivar ONVIF de forma predeterminada. Aseg\u00farese de que ONVIF est\u00e9 activado en la configuraci\u00f3n de la c\u00e1mara.", "title": "Configuraci\u00f3n del dispositivo ONVIF" } diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index 5cd00abc216..153512cb83b 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Se solicita al dispositivo que se encienda" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Permitir el uso del servicio de notificaci\u00f3n de datos." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/es.json b/homeassistant/components/sonos/translations/es.json index 41d84a932ee..3560280c90e 100644 --- a/homeassistant/components/sonos/translations/es.json +++ b/homeassistant/components/sonos/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "No se encontraron dispositivos en la red", + "not_sonos_device": "El dispositivo descubierto no es un dispositivo Sonos", "single_instance_allowed": "S\u00f3lo se necesita una \u00fanica configuraci\u00f3n de Sonos." }, "step": { diff --git a/homeassistant/components/sonos/translations/it.json b/homeassistant/components/sonos/translations/it.json index 1a646649a1b..e10ba5079b6 100644 --- a/homeassistant/components/sonos/translations/it.json +++ b/homeassistant/components/sonos/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Nessun dispositivo trovato sulla rete", + "not_sonos_device": "Il dispositivo rilevato non \u00e8 un dispositivo Sonos", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "step": { diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 1084f0b4722..a758d81e553 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -60,6 +60,9 @@ "trigger_type": { "event.notification.entry_control": "Ha enviat una notificaci\u00f3 de control d'entrada", "event.notification.notification": "Ha enviat una notificaci\u00f3", + "event.value_notification.basic": "Esdeveniment CC b\u00e0sic a {subtype}", + "event.value_notification.central_scene": "Acci\u00f3 d'escena central a {subtype}", + "event.value_notification.scene_activation": "Activaci\u00f3 d'escena a {subtype}", "state.node_status": "L'estat del node ha canviat" } }, diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 1e5b07ec171..b337fd5450e 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -51,5 +51,47 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Valor del par\u00e1metro de configuraci\u00f3n {subtype}", + "node_status": "Estado del nodo", + "value": "Valor actual de un valor Z-Wave" + }, + "trigger_type": { + "event.notification.entry_control": "Envi\u00f3 una notificaci\u00f3n de control de entrada", + "event.notification.notification": "Envi\u00f3 una notificaci\u00f3n", + "event.value_notification.basic": "Evento CC b\u00e1sico en {subtype}", + "event.value_notification.central_scene": "Acci\u00f3n de escena central en {subtype}", + "event.value_notification.scene_activation": "Activaci\u00f3n de escena en {subtype}", + "state.node_status": "El estado del nodo ha cambiado" + } + }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", + "addon_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n del complemento Z-Wave JS.", + "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", + "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", + "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", + "different_device": "El dispositivo USB conectado no es el mismo que el configurado anteriormente para esta entrada de configuraci\u00f3n. Por favor, cree una nueva entrada de configuraci\u00f3n para el nuevo dispositivo." + }, + "error": { + "invalid_ws_url": "URL de websocket no v\u00e1lida" + }, + "progress": { + "install_addon": "Por favor, espere mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", + "start_addon": "Por favor, espere mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar algunos segundos." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emular el hardware", + "log_level": "Nivel de registro", + "network_key": "Clave de red" + }, + "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index 71832e4882b..7c79cb304ef 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -51,6 +51,21 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Valore del parametro di configurazione {subtype}", + "node_status": "Stato del nodo", + "value": "Valore corrente di un valore Z-Wave" + }, + "trigger_type": { + "event.notification.entry_control": "Inviata una notifica di controllo delle entrate", + "event.notification.notification": "Inviata una notifica", + "event.value_notification.basic": "Evento CC di base su {subtype}", + "event.value_notification.central_scene": "Azione della scena centrale su {subtype}", + "event.value_notification.scene_activation": "Attivazione scena su {subtype}", + "state.node_status": "Lo stato del nodo \u00e8 cambiato" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", From 24cc5c8a0aad07a49afcfbc95bfefe876330cf29 Mon Sep 17 00:00:00 2001 From: kpine Date: Fri, 16 Jul 2021 23:54:11 -0700 Subject: [PATCH 331/818] Replace local Barrier CC constants with library enums (#53109) --- homeassistant/components/zwave_js/cover.py | 22 +++++++-------------- homeassistant/components/zwave_js/switch.py | 13 ++++++------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index ec38fb1f3af..f8e575521dc 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -5,6 +5,7 @@ import logging from typing import Any from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import BarrierState from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( @@ -29,15 +30,6 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) -BARRIER_TARGET_CLOSE = 0 -BARRIER_TARGET_OPEN = 255 - -BARRIER_STATE_CLOSED = 0 -BARRIER_STATE_CLOSING = 252 -BARRIER_STATE_STOPPED = 253 -BARRIER_STATE_OPENING = 254 -BARRIER_STATE_OPEN = 255 - async def async_setup_entry( hass: HomeAssistant, @@ -172,14 +164,14 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): """Return if the cover is opening or not.""" if self.info.primary_value.value is None: return None - return bool(self.info.primary_value.value == BARRIER_STATE_OPENING) + return bool(self.info.primary_value.value == BarrierState.OPENING) @property def is_closing(self) -> bool | None: """Return if the cover is closing or not.""" if self.info.primary_value.value is None: return None - return bool(self.info.primary_value.value == BARRIER_STATE_CLOSING) + return bool(self.info.primary_value.value == BarrierState.CLOSING) @property def is_closed(self) -> bool | None: @@ -190,15 +182,15 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): # issuing an open cover command. Return None in this case which # produces an unknown state and allows it to be resolved with an open # command. - if self.info.primary_value.value == BARRIER_STATE_STOPPED: + if self.info.primary_value.value == BarrierState.STOPPED: return None - return bool(self.info.primary_value.value == BARRIER_STATE_CLOSED) + return bool(self.info.primary_value.value == BarrierState.CLOSED) async def async_open_cover(self, **kwargs: Any) -> None: """Open the garage door.""" - await self.info.node.async_set_value(self._target_state, BARRIER_TARGET_OPEN) + await self.info.node.async_set_value(self._target_state, BarrierState.OPEN) async def async_close_cover(self, **kwargs: Any) -> None: """Close the garage door.""" - await self.info.node.async_set_value(self._target_state, BARRIER_TARGET_CLOSE) + await self.info.node.async_set_value(self._target_state, BarrierState.CLOSED) diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index ae98ddc563a..0bc6b8d5349 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -5,6 +5,7 @@ import logging from typing import Any from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import BarrierEventSignalingSubsystemState from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -19,10 +20,6 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) -BARRIER_EVENT_SIGNALING_OFF = 0 -BARRIER_EVENT_SIGNALING_ON = 255 - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -108,7 +105,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.info.node.async_set_value( - self.info.primary_value, BARRIER_EVENT_SIGNALING_ON + self.info.primary_value, BarrierEventSignalingSubsystemState.ON ) # this value is not refreshed, so assume success self._state = True @@ -117,7 +114,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.info.node.async_set_value( - self.info.primary_value, BARRIER_EVENT_SIGNALING_OFF + self.info.primary_value, BarrierEventSignalingSubsystemState.OFF ) # this value is not refreshed, so assume success self._state = False @@ -127,4 +124,6 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): def _update_state(self) -> None: self._state = None if self.info.primary_value.value is not None: - self._state = self.info.primary_value.value == BARRIER_EVENT_SIGNALING_ON + self._state = ( + self.info.primary_value.value == BarrierEventSignalingSubsystemState.ON + ) From 7fe3f78c24c52f893edefe12d3ee037840a643b6 Mon Sep 17 00:00:00 2001 From: Brett Date: Sat, 17 Jul 2021 21:45:29 +1000 Subject: [PATCH 332/818] Bump library version for Advantage Air (#52813) * Bump library version * Bump version to 0.2.5 * Add tests to cover this edge case --- .../components/advantage_air/manifest.json | 10 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/advantage_air/test_cover.py | 32 +++++++++++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 750d5457e17..6390ccea39c 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -3,8 +3,12 @@ "name": "Advantage Air", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/advantage_air", - "codeowners": ["@Bre77"], - "requirements": ["advantage_air==0.2.1"], + "codeowners": [ + "@Bre77" + ], + "requirements": [ + "advantage_air==0.2.5" + ], "quality_scale": "platinum", "iot_class": "local_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 538523b1f28..2e02ad5bf19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -114,7 +114,7 @@ adext==0.4.2 adguardhome==0.5.0 # homeassistant.components.advantage_air -advantage_air==0.2.1 +advantage_air==0.2.5 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 872938f50c7..7b371b2ea71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -57,7 +57,7 @@ adext==0.4.2 adguardhome==0.5.0 # homeassistant.components.advantage_air -advantage_air==0.2.1 +advantage_air==0.2.5 # homeassistant.components.agent_dvr agent-py==0.0.23 diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 29f0d288fdb..363db076ada 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -112,3 +112,35 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert data["ac2"]["zones"]["z01"]["state"] == ADVANTAGE_AIR_STATE_CLOSE assert aioclient_mock.mock_calls[-1][0] == "GET" assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + # Test controlling multiple Cover Zone Entity + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + { + ATTR_ENTITY_ID: [ + "cover.zone_open_without_sensor", + "cover.zone_closed_without_sensor", + ] + }, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 11 + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac2"]["zones"]["z01"]["state"] == ADVANTAGE_AIR_STATE_CLOSE + assert data["ac2"]["zones"]["z02"]["state"] == ADVANTAGE_AIR_STATE_CLOSE + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + { + ATTR_ENTITY_ID: [ + "cover.zone_open_without_sensor", + "cover.zone_closed_without_sensor", + ] + }, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 13 + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac2"]["zones"]["z01"]["state"] == ADVANTAGE_AIR_STATE_OPEN + assert data["ac2"]["zones"]["z02"]["state"] == ADVANTAGE_AIR_STATE_OPEN From 8f3166a955269387c589e10e8e200132064011c4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jul 2021 00:10:14 +0000 Subject: [PATCH 333/818] [ci skip] Translation update --- .../components/cloudflare/translations/es.json | 2 ++ .../components/coinbase/translations/es.json | 12 +++++++++++- .../devolo_home_control/translations/es.json | 6 ++++-- .../components/dsmr/translations/es.json | 16 ++++++++++++++-- .../forecast_solar/translations/es.json | 6 ++++++ .../components/freedompro/translations/es.json | 10 ++++++++++ .../components/nmap_tracker/translations/es.json | 13 ++++++++++++- .../components/onvif/translations/es.json | 7 +++++++ .../components/xiaomi_miio/translations/es.json | 3 ++- .../components/zwave_js/translations/es.json | 12 ++++++++++-- 10 files changed, 78 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index 8ef8fd15dd4..0647609e4e8 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown": "Error inesperado" }, @@ -13,6 +14,7 @@ "step": { "reauth_confirm": { "data": { + "api_token": "Token API", "description": "Vuelva a autenticarse con su cuenta de Cloudflare." } }, diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index d5fee8ed5b3..9948ef57020 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "user": { "data": { + "api_key": "Clave API", "api_token": "Secreto de la API", "currencies": "Saldo de la cuenta Monedas", "exchange_rates": "Tipos de cambio" @@ -15,7 +24,8 @@ "options": { "error": { "currency_unavaliable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", - "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados." + "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", + "unknown": "Error inesperado" }, "step": { "init": { diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index fe862c1c01d..b4a7a873aaa 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "reauth_failed": "Por favor, utiliza el mismo usuario de mydevolo que antes." }, "step": { "user": { diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 024d5a55a00..880f378c7c3 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -1,14 +1,23 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_communicate": "No se ha podido comunicar", + "cannot_connect": "No se pudo conectar" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_communicate": "No se ha podido comunicar", + "cannot_connect": "No se pudo conectar" }, "step": { "one": "Vac\u00edo", "other": "Vac\u00edo", "setup_network": { "data": { - "dsmr_version": "Seleccione la versi\u00f3n de DSMR" + "dsmr_version": "Seleccione la versi\u00f3n de DSMR", + "host": "Host", + "port": "Puerto" }, "title": "Seleccione la direcci\u00f3n de la conexi\u00f3n" }, @@ -20,6 +29,9 @@ "title": "Dispositivo" }, "setup_serial_manual_path": { + "data": { + "port": "Ruta del dispositivo USB" + }, "title": "Ruta" }, "user": { diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 41846c60ab6..2189cb91f77 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -2,6 +2,12 @@ "options": { "step": { "init": { + "data": { + "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", + "damping": "Factor de amortiguaci\u00f3n: ajusta los resultados por la ma\u00f1ana y por la noche", + "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", + "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares" + }, "description": "Estos valores permiten ajustar el resultado de Solar.Forecast. Consulte la documentaci\u00f3n si un campo no est\u00e1 claro." } } diff --git a/homeassistant/components/freedompro/translations/es.json b/homeassistant/components/freedompro/translations/es.json index 5325f4216f3..b6f8afeaf6d 100644 --- a/homeassistant/components/freedompro/translations/es.json +++ b/homeassistant/components/freedompro/translations/es.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, "step": { "user": { + "data": { + "api_key": "Clave API" + }, "description": "Ingrese la clave API obtenida de https://home.freedompro.eu", "title": "Clave API de Freedompro" } diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index 1d0d2252a5a..d5c3d71321f 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, "error": { "invalid_hosts": "Hosts no v\u00e1lidos" }, @@ -16,12 +19,20 @@ } }, "options": { + "error": { + "invalid_hosts": "Hosts no v\u00e1lidos" + }, "step": { "init": { "data": { + "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", + "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", + "hosts": "Direcciones de red (separadas por comas) para escanear", "interval_seconds": "Intervalo de exploraci\u00f3n", + "scan_options": "Opciones de escaneo configurables sin procesar para Nmap", "track_new_devices": "Seguimiento de nuevos dispositivos" - } + }, + "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." } } }, diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 8b44955efdc..d5c9688b875 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -19,6 +19,13 @@ "title": "Configurar la autenticaci\u00f3n" }, "configure": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + }, "title": "Configurar dispositivo ONVIF" }, "configure_profile": { diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 0193cd4a39a..26f5b3937b6 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -57,7 +57,8 @@ "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan." + "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan.", + "title": "Volver a autenticar la integraci\u00f3n" }, "select": { "data": { diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index b337fd5450e..99ffee8270d 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -73,10 +73,14 @@ "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "different_device": "El dispositivo USB conectado no es el mismo que el configurado anteriormente para esta entrada de configuraci\u00f3n. Por favor, cree una nueva entrada de configuraci\u00f3n para el nuevo dispositivo." }, "error": { - "invalid_ws_url": "URL de websocket no v\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_ws_url": "URL de websocket no v\u00e1lida", + "unknown": "Error inesperado" }, "progress": { "install_addon": "Por favor, espere mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", @@ -87,9 +91,13 @@ "data": { "emulate_hardware": "Emular el hardware", "log_level": "Nivel de registro", - "network_key": "Clave de red" + "network_key": "Clave de red", + "usb_path": "Ruta del dispositivo USB" }, "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" + }, + "install_addon": { + "title": "La instalaci\u00f3n del complemento Z-Wave JS ha comenzado" } } }, From b63e38f538091e2fb7c6e83464fb29da561b16ae Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sun, 18 Jul 2021 02:24:09 -0400 Subject: [PATCH 334/818] Add more Gree switches (#49629) * Support additional switch for gree devices * Undo some changes not related to review * Retry build * Back to Gree 0.11.7 --- homeassistant/components/gree/entity.py | 37 +++++++ homeassistant/components/gree/switch.py | 130 +++++++++++++++++++----- tests/components/gree/common.py | 1 + tests/components/gree/test_switch.py | 89 ++++++++++++---- 4 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 homeassistant/components/gree/entity.py diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py new file mode 100644 index 00000000000..0753a780f4b --- /dev/null +++ b/homeassistant/components/gree/entity.py @@ -0,0 +1,37 @@ +"""Entity object for shared properties of Gree entities.""" +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .bridge import DeviceDataUpdateCoordinator +from .const import DOMAIN + + +class GreeEntity(CoordinatorEntity): + """Generic Gree entity (base class).""" + + def __init__(self, coordinator: DeviceDataUpdateCoordinator, desc: str) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._desc = desc + self._name = f"{coordinator.device.device_info.name}" + self._mac = coordinator.device.device_info.mac + + @property + def name(self): + """Return the name of the node.""" + return f"{self._name} {self._desc}" + + @property + def unique_id(self): + """Return the unique id based for the node.""" + return f"{self._mac}_{self._desc}" + + @property + def device_info(self): + """Return info about the device.""" + return { + "identifiers": {(DOMAIN, self._mac)}, + "name": self._name, + "manufacturer": "Gree", + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + } diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 7f659d7e64b..f8a5b4c0b3d 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -3,11 +3,10 @@ from __future__ import annotations from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN +from .entity import GreeEntity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -16,7 +15,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def init_device(coordinator): """Register the device.""" - async_add_entities([GreeSwitchEntity(coordinator)]) + async_add_entities( + [ + GreePanelLightSwitchEntity(coordinator), + GreeQuietModeSwitchEntity(coordinator), + GreeFreshAirSwitchEntity(coordinator), + GreeXFanSwitchEntity(coordinator), + ] + ) for coordinator in hass.data[DOMAIN][COORDINATORS]: init_device(coordinator) @@ -26,40 +32,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class GreeSwitchEntity(CoordinatorEntity, SwitchEntity): - """Representation of a Gree HVAC device.""" +class GreePanelLightSwitchEntity(GreeEntity, SwitchEntity): + """Representation of the front panel light on the device.""" def __init__(self, coordinator): """Initialize the Gree device.""" - super().__init__(coordinator) - self._name = coordinator.device.device_info.name + " Panel Light" - self._mac = coordinator.device.device_info.mac - - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique id for the device.""" - return f"{self._mac}-panel-light" + super().__init__(coordinator, "Panel Light") @property def icon(self) -> str | None: """Return the icon for the device.""" return "mdi:lightbulb" - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self._name, - "identifiers": {(DOMAIN, self._mac)}, - "manufacturer": "Gree", - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - } - @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" @@ -81,3 +65,93 @@ class GreeSwitchEntity(CoordinatorEntity, SwitchEntity): self.coordinator.device.light = False await self.coordinator.push_state_update() self.async_write_ha_state() + + +class GreeQuietModeSwitchEntity(GreeEntity, SwitchEntity): + """Representation of the quiet mode state of the device.""" + + def __init__(self, coordinator): + """Initialize the Gree device.""" + super().__init__(coordinator, "Quiet") + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_SWITCH + + @property + def is_on(self) -> bool: + """Return if the state is turned on.""" + return self.coordinator.device.quiet + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self.coordinator.device.quiet = True + await self.coordinator.push_state_update() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self.coordinator.device.quiet = False + await self.coordinator.push_state_update() + self.async_write_ha_state() + + +class GreeFreshAirSwitchEntity(GreeEntity, SwitchEntity): + """Representation of the fresh air mode state of the device.""" + + def __init__(self, coordinator): + """Initialize the Gree device.""" + super().__init__(coordinator, "Fresh Air") + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_SWITCH + + @property + def is_on(self) -> bool: + """Return if the state is turned on.""" + return self.coordinator.device.fresh_air + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self.coordinator.device.fresh_air = True + await self.coordinator.push_state_update() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self.coordinator.device.fresh_air = False + await self.coordinator.push_state_update() + self.async_write_ha_state() + + +class GreeXFanSwitchEntity(GreeEntity, SwitchEntity): + """Representation of the extra fan mode state of the device.""" + + def __init__(self, coordinator): + """Initialize the Gree device.""" + super().__init__(coordinator, "XFan") + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_SWITCH + + @property + def is_on(self) -> bool: + """Return if the state is turned on.""" + return self.coordinator.device.xfan + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self.coordinator.device.xfan = True + await self.coordinator.push_state_update() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self.coordinator.device.xfan = False + await self.coordinator.push_state_update() + self.async_write_ha_state() diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index 2c9c295da1c..40403377957 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -62,6 +62,7 @@ def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc1122 horizontal_swing=0, vertical_swing=0, target_temperature=25, + current_temperature=25, power=False, sleep=False, quiet=False, diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py index 39ad536880c..3347fac00f5 100644 --- a/tests/components/gree/test_switch.py +++ b/tests/components/gree/test_switch.py @@ -1,5 +1,6 @@ """Tests for gree component.""" from greeclimate.exceptions import DeviceTimeoutError +import pytest from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN from homeassistant.components.switch import DOMAIN @@ -16,7 +17,10 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -ENTITY_ID = f"{DOMAIN}.fake_device_1_panel_light" +ENTITY_ID_LIGHT_PANEL = f"{DOMAIN}.fake_device_1_panel_light" +ENTITY_ID_QUIET = f"{DOMAIN}.fake_device_1_quiet" +ENTITY_ID_FRESH_AIR = f"{DOMAIN}.fake_device_1_fresh_air" +ENTITY_ID_XFAN = f"{DOMAIN}.fake_device_1_xfan" async def async_setup_gree(hass): @@ -26,23 +30,41 @@ async def async_setup_gree(hass): await hass.async_block_till_done() -async def test_send_panel_light_on(hass): +@pytest.mark.parametrize( + "entity", + [ + ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_QUIET, + ENTITY_ID_FRESH_AIR, + ENTITY_ID_XFAN, + ], +) +async def test_send_switch_on(hass, entity): """Test for sending power on command to the device.""" await async_setup_gree(hass) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - state = hass.states.get(ENTITY_ID) + state = hass.states.get(entity) assert state is not None assert state.state == STATE_ON -async def test_send_panel_light_on_device_timeout(hass, device): +@pytest.mark.parametrize( + "entity", + [ + ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_QUIET, + ENTITY_ID_FRESH_AIR, + ENTITY_ID_XFAN, + ], +) +async def test_send_switch_on_device_timeout(hass, device, entity): """Test for sending power on command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -51,32 +73,50 @@ async def test_send_panel_light_on_device_timeout(hass, device): assert await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - state = hass.states.get(ENTITY_ID) + state = hass.states.get(entity) assert state is not None assert state.state == STATE_ON -async def test_send_panel_light_off(hass): +@pytest.mark.parametrize( + "entity", + [ + ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_QUIET, + ENTITY_ID_FRESH_AIR, + ENTITY_ID_XFAN, + ], +) +async def test_send_switch_off(hass, entity): """Test for sending power on command to the device.""" await async_setup_gree(hass) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - state = hass.states.get(ENTITY_ID) + state = hass.states.get(entity) assert state is not None assert state.state == STATE_OFF -async def test_send_panel_light_toggle(hass): +@pytest.mark.parametrize( + "entity", + [ + ENTITY_ID_LIGHT_PANEL, + ENTITY_ID_QUIET, + ENTITY_ID_FRESH_AIR, + ENTITY_ID_XFAN, + ], +) +async def test_send_switch_toggle(hass, entity): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -84,11 +124,11 @@ async def test_send_panel_light_toggle(hass): assert await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - state = hass.states.get(ENTITY_ID) + state = hass.states.get(entity) assert state is not None assert state.state == STATE_ON @@ -96,11 +136,11 @@ async def test_send_panel_light_toggle(hass): assert await hass.services.async_call( DOMAIN, SERVICE_TOGGLE, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - state = hass.states.get(ENTITY_ID) + state = hass.states.get(entity) assert state is not None assert state.state == STATE_OFF @@ -108,17 +148,26 @@ async def test_send_panel_light_toggle(hass): assert await hass.services.async_call( DOMAIN, SERVICE_TOGGLE, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - state = hass.states.get(ENTITY_ID) + state = hass.states.get(entity) assert state is not None assert state.state == STATE_ON -async def test_panel_light_name(hass): +@pytest.mark.parametrize( + "entity,name", + [ + (ENTITY_ID_LIGHT_PANEL, "Panel Light"), + (ENTITY_ID_QUIET, "Quiet"), + (ENTITY_ID_FRESH_AIR, "Fresh Air"), + (ENTITY_ID_XFAN, "XFan"), + ], +) +async def test_entity_name(hass, entity, name): """Test for name property.""" await async_setup_gree(hass) - state = hass.states.get(ENTITY_ID) - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake-device-1 Panel Light" + state = hass.states.get(entity) + assert state.attributes[ATTR_FRIENDLY_NAME] == f"fake-device-1 {name}" From 78f4a49b739b138c51ff46a011c064ab01d24235 Mon Sep 17 00:00:00 2001 From: Ben <512997+benleb@users.noreply.github.com> Date: Sun, 18 Jul 2021 08:49:07 +0200 Subject: [PATCH 335/818] Bump surepy to 0.7.0 (#53123) --- homeassistant/components/surepetcare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 1f0804e0581..ee97e1ac627 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,6 +3,6 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb", "@danielhiversen"], - "requirements": ["surepy==0.6.0"], + "requirements": ["surepy==0.7.0"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2e02ad5bf19..b5de00118ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2205,7 +2205,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.6.0 +surepy==0.7.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b371b2ea71..0d9f7ee19ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1210,7 +1210,7 @@ subarulink==0.3.12 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.6.0 +surepy==0.7.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.2 From 71a8ae3016aba7c1929ff47ab22e4f80b55b7cc3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 18 Jul 2021 14:43:47 +0200 Subject: [PATCH 336/818] Add new unit types for better type checking (#53124) * Add new unit types * Update helper functions * Update components * Update lcn climate --- homeassistant/components/climate/__init__.py | 5 +- .../components/climate/device_trigger.py | 2 + .../components/devolo_home_control/climate.py | 4 +- homeassistant/components/esphome/climate.py | 3 +- homeassistant/components/fritzbox/climate.py | 3 +- homeassistant/components/lcn/climate.py | 10 +- homeassistant/components/mysensors/climate.py | 9 +- homeassistant/components/zwave_js/climate.py | 3 +- homeassistant/const.py | 277 ++++++++++-------- homeassistant/helpers/temperature.py | 7 +- homeassistant/util/distance.py | 11 +- homeassistant/util/pressure.py | 15 +- homeassistant/util/temperature.py | 6 +- homeassistant/util/unit_system.py | 39 ++- homeassistant/util/volume.py | 12 +- 15 files changed, 254 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index cc26bcc9bcc..0b80402b3ec 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, TEMP_CELSIUS, + UnitTemperatureT, ) from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv @@ -195,7 +196,7 @@ class ClimateEntity(Entity): _attr_target_temperature_low: float | None _attr_target_temperature_step: float | None = None _attr_target_temperature: float | None = None - _attr_temperature_unit: str + _attr_temperature_unit: UnitTemperatureT @property def state(self) -> str: @@ -303,7 +304,7 @@ class ClimateEntity(Entity): return data @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement used by the platform.""" return self._attr_temperature_unit diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 1b5127d7d4a..db8eeedd54c 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, PERCENTAGE, + UnitT, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry @@ -172,6 +173,7 @@ async def async_get_trigger_capabilities(hass: HomeAssistant, config): ) } + unit_of_measurement: UnitT if trigger_type == "current_temperature_changed": unit_of_measurement = hass.config.units.temperature_unit else: diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 6b890544da5..cad439eb284 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitTemperatureT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,7 +105,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit return SUPPORT_TARGET_TEMPERATURE @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the supported unit of temperature.""" return TEMP_CELSIUS diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 218f0fb319b..016d197e8d9 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -65,6 +65,7 @@ from homeassistant.const import ( PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS, + UnitTemperatureT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -166,7 +167,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return PRECISION_TENTHS @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement used by the platform.""" return TEMP_CELSIUS diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index c50e0d4f270..7f3ee76b48e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PRECISION_HALVES, TEMP_CELSIUS, + UnitTemperatureT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -97,7 +98,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): return self.device.present # type: ignore [no-any-return] @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement that is used.""" return TEMP_CELSIUS diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 0a595076a8d..e453f26c25c 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -18,6 +18,9 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UnitTemperatureT, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -107,9 +110,12 @@ class LcnClimate(LcnEntity, ClimateEntity): return const.SUPPORT_TARGET_TEMPERATURE @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement.""" - return cast(str, self.unit.value) + # Config schema only allows for: TEMP_CELSIUS and TEMP_FAHRENHEIT + if self.unit == pypck.lcn_defs.VarUnit.FAHRENHEIT: + return TEMP_FAHRENHEIT + return TEMP_CELSIUS @property def current_temperature(self) -> float | None: diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 5dd52673581..5afb4a803d2 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -19,7 +19,12 @@ from homeassistant.components.climate.const import ( ) from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + ATTR_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UnitTemperatureT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -91,7 +96,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): return features @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement.""" return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 1621e87cfab..62da8cc8c80 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -53,6 +53,7 @@ from homeassistant.const import ( PRECISION_TENTHS, TEMP_CELSIUS, TEMP_FAHRENHEIT, + UnitTemperatureT, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -248,7 +249,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement used by the platform.""" if ( self._unit_value diff --git a/homeassistant/const.py b/homeassistant/const.py index 35b946fc6ab..3f07196d570 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" from __future__ import annotations -from typing import Final +from typing import Final, NewType MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 @@ -393,166 +393,212 @@ ATTR_DEVICE_CLASS: Final = "device_class" # Temperature attribute ATTR_TEMPERATURE: Final = "temperature" + # #### UNITS OF MEASUREMENT #### +UnitT = NewType("UnitT", str) + # Power units -POWER_WATT: Final = "W" -POWER_KILO_WATT: Final = "kW" +UnitPowerT = NewType("UnitPowerT", UnitT) +POWER_WATT: Final[UnitPowerT] = UnitPowerT(UnitT("W")) +POWER_KILO_WATT: Final[UnitPowerT] = UnitPowerT(UnitT("kW")) # Voltage units -VOLT: Final = "V" +VOLT: Final[UnitT] = UnitT("V") # Energy units -ENERGY_WATT_HOUR: Final = "Wh" -ENERGY_KILO_WATT_HOUR: Final = "kWh" +UnitEnergyT = NewType("UnitEnergyT", UnitT) +ENERGY_WATT_HOUR: Final[UnitEnergyT] = UnitEnergyT(UnitT("Wh")) +ENERGY_KILO_WATT_HOUR: Final[UnitEnergyT] = UnitEnergyT(UnitT("kWh")) # Electrical units -ELECTRICAL_CURRENT_AMPERE: Final = "A" -ELECTRICAL_VOLT_AMPERE: Final = "VA" +ELECTRICAL_CURRENT_AMPERE: Final[UnitT] = UnitT("A") +ELECTRICAL_VOLT_AMPERE: Final[UnitT] = UnitT("VA") # Degree units -DEGREE: Final = "°" +DEGREE: Final[UnitT] = UnitT("°") # Currency units -CURRENCY_EURO: Final = "€" -CURRENCY_DOLLAR: Final = "$" -CURRENCY_CENT: Final = "¢" +UnitCurrencyT = NewType("UnitCurrencyT", UnitT) +CURRENCY_EURO: Final[UnitCurrencyT] = UnitCurrencyT(UnitT("€")) +CURRENCY_DOLLAR: Final[UnitCurrencyT] = UnitCurrencyT(UnitT("$")) +CURRENCY_CENT: Final[UnitCurrencyT] = UnitCurrencyT(UnitT("¢")) # Temperature units -TEMP_CELSIUS: Final = "°C" -TEMP_FAHRENHEIT: Final = "°F" -TEMP_KELVIN: Final = "K" +UnitTemperatureT = NewType("UnitTemperatureT", UnitT) +TEMP_CELSIUS: Final[UnitTemperatureT] = UnitTemperatureT(UnitT("°C")) +TEMP_FAHRENHEIT: Final[UnitTemperatureT] = UnitTemperatureT(UnitT("°F")) +TEMP_KELVIN: Final[UnitTemperatureT] = UnitTemperatureT(UnitT("K")) # Time units -TIME_MICROSECONDS: Final = "μs" -TIME_MILLISECONDS: Final = "ms" -TIME_SECONDS: Final = "s" -TIME_MINUTES: Final = "min" -TIME_HOURS: Final = "h" -TIME_DAYS: Final = "d" -TIME_WEEKS: Final = "w" -TIME_MONTHS: Final = "m" -TIME_YEARS: Final = "y" +UnitTimeT = NewType("UnitTimeT", UnitT) +TIME_MICROSECONDS: Final[UnitTimeT] = UnitTimeT(UnitT("μs")) +TIME_MILLISECONDS: Final[UnitTimeT] = UnitTimeT(UnitT("ms")) +TIME_SECONDS: Final[UnitTimeT] = UnitTimeT(UnitT("s")) +TIME_MINUTES: Final[UnitTimeT] = UnitTimeT(UnitT("min")) +TIME_HOURS: Final[UnitTimeT] = UnitTimeT(UnitT("h")) +TIME_DAYS: Final[UnitTimeT] = UnitTimeT(UnitT("d")) +TIME_WEEKS: Final[UnitTimeT] = UnitTimeT(UnitT("w")) +TIME_MONTHS: Final[UnitTimeT] = UnitTimeT(UnitT("m")) +TIME_YEARS: Final[UnitTimeT] = UnitTimeT(UnitT("y")) # Length units -LENGTH_MILLIMETERS: Final = "mm" -LENGTH_CENTIMETERS: Final = "cm" -LENGTH_METERS: Final = "m" -LENGTH_KILOMETERS: Final = "km" +UnitLengthT = NewType("UnitLengthT", UnitT) +LENGTH_MILLIMETERS: Final[UnitLengthT] = UnitLengthT(UnitT("mm")) +LENGTH_CENTIMETERS: Final[UnitLengthT] = UnitLengthT(UnitT("cm")) +LENGTH_METERS: Final[UnitLengthT] = UnitLengthT(UnitT("m")) +LENGTH_KILOMETERS: Final[UnitLengthT] = UnitLengthT(UnitT("km")) -LENGTH_INCHES: Final = "in" -LENGTH_FEET: Final = "ft" -LENGTH_YARD: Final = "yd" -LENGTH_MILES: Final = "mi" +LENGTH_INCHES: Final[UnitLengthT] = UnitLengthT(UnitT("in")) +LENGTH_FEET: Final[UnitLengthT] = UnitLengthT(UnitT("ft")) +LENGTH_YARD: Final[UnitLengthT] = UnitLengthT(UnitT("yd")) +LENGTH_MILES: Final[UnitLengthT] = UnitLengthT(UnitT("mi")) # Frequency units -FREQUENCY_HERTZ: Final = "Hz" -FREQUENCY_GIGAHERTZ: Final = "GHz" +UnitFrequencyT = NewType("UnitFrequencyT", UnitT) +FREQUENCY_HERTZ: Final[UnitFrequencyT] = UnitFrequencyT(UnitT("Hz")) +FREQUENCY_GIGAHERTZ: Final[UnitFrequencyT] = UnitFrequencyT(UnitT("GHz")) # Pressure units -PRESSURE_PA: Final = "Pa" -PRESSURE_HPA: Final = "hPa" -PRESSURE_BAR: Final = "bar" -PRESSURE_MBAR: Final = "mbar" -PRESSURE_INHG: Final = "inHg" -PRESSURE_PSI: Final = "psi" +UnitPressureT = NewType("UnitPressureT", UnitT) +PRESSURE_PA: Final[UnitPressureT] = UnitPressureT(UnitT("Pa")) +PRESSURE_HPA: Final[UnitPressureT] = UnitPressureT(UnitT("hPa")) +PRESSURE_BAR: Final[UnitPressureT] = UnitPressureT(UnitT("bar")) +PRESSURE_MBAR: Final[UnitPressureT] = UnitPressureT(UnitT("mbar")) +PRESSURE_INHG: Final[UnitPressureT] = UnitPressureT(UnitT("inHg")) +PRESSURE_PSI: Final[UnitPressureT] = UnitPressureT(UnitT("psi")) # Volume units -VOLUME_LITERS: Final = "L" -VOLUME_MILLILITERS: Final = "mL" -VOLUME_CUBIC_METERS: Final = "m³" -VOLUME_CUBIC_FEET: Final = "ft³" +UnitVolumeT = NewType("UnitVolumeT", UnitT) +VOLUME_LITERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("L")) +VOLUME_MILLILITERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("mL")) +VOLUME_CUBIC_METERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("m³")) +VOLUME_CUBIC_FEET: Final[UnitVolumeT] = UnitVolumeT(UnitT("ft³")) -VOLUME_GALLONS: Final = "gal" -VOLUME_FLUID_OUNCE: Final = "fl. oz." +VOLUME_GALLONS: Final[UnitVolumeT] = UnitVolumeT(UnitT("gal")) +VOLUME_FLUID_OUNCE: Final[UnitVolumeT] = UnitVolumeT(UnitT("fl. oz.")) # Volume Flow Rate units -VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h" -VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = "ft³/m" +UnitVolumeFlowT = NewType("UnitVolumeFlowT", UnitT) +VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final[UnitVolumeFlowT] = UnitVolumeFlowT( + UnitT("m³/h") +) +VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final[UnitVolumeFlowT] = UnitVolumeFlowT( + UnitT("ft³/m") +) # Area units -AREA_SQUARE_METERS: Final = "m²" +UnitAreaT = NewType("UnitAreaT", UnitT) +AREA_SQUARE_METERS: Final[UnitAreaT] = UnitAreaT(UnitT("m²")) # Mass units -MASS_GRAMS: Final = "g" -MASS_KILOGRAMS: Final = "kg" -MASS_MILLIGRAMS: Final = "mg" -MASS_MICROGRAMS: Final = "µg" +UnitMassT = NewType("UnitMassT", UnitT) +MASS_GRAMS: Final[UnitMassT] = UnitMassT(UnitT("g")) +MASS_KILOGRAMS: Final[UnitMassT] = UnitMassT(UnitT("kg")) +MASS_MILLIGRAMS: Final[UnitMassT] = UnitMassT(UnitT("mg")) +MASS_MICROGRAMS: Final[UnitMassT] = UnitMassT(UnitT("µg")) -MASS_OUNCES: Final = "oz" -MASS_POUNDS: Final = "lb" +MASS_OUNCES: Final[UnitMassT] = UnitMassT(UnitT("oz")) +MASS_POUNDS: Final[UnitMassT] = UnitMassT(UnitT("lb")) # Conductivity units -CONDUCTIVITY: Final = "µS/cm" +CONDUCTIVITY: Final[UnitT] = UnitT("µS/cm") # Light units -LIGHT_LUX: Final = "lx" +LIGHT_LUX: Final[UnitT] = UnitT("lx") # UV Index units -UV_INDEX: Final = "UV index" +UV_INDEX: Final[UnitT] = UnitT("UV index") # Percentage units -PERCENTAGE: Final = "%" +PERCENTAGE: Final[UnitT] = UnitT("%") # Irradiation units -IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" -IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)" +UnitIrradiationT = NewType("UnitIrradiationT", UnitT) +IRRADIATION_WATTS_PER_SQUARE_METER: Final[UnitIrradiationT] = UnitIrradiationT( + UnitT("W/m²") +) +IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final[UnitIrradiationT] = UnitIrradiationT( + UnitT("BTU/(h×ft²)") +) # Precipitation units -PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" +PRECIPITATION_MILLIMETERS_PER_HOUR: Final[UnitT] = UnitT("mm/h") # Concentration units -CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" -CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" -CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" -CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" -CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" -CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" +UnitConcentrationT = NewType("UnitConcentrationT", UnitT) +CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final[ + UnitConcentrationT +] = UnitConcentrationT(UnitT("µg/m³")) +CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final[ + UnitConcentrationT +] = UnitConcentrationT(UnitT("mg/m³")) +CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final[UnitConcentrationT] = UnitConcentrationT( + UnitT("μg/ft³") +) +CONCENTRATION_PARTS_PER_CUBIC_METER: Final[UnitConcentrationT] = UnitConcentrationT( + UnitT("p/m³") +) +CONCENTRATION_PARTS_PER_MILLION: Final[UnitConcentrationT] = UnitConcentrationT( + UnitT("ppm") +) +CONCENTRATION_PARTS_PER_BILLION: Final[UnitConcentrationT] = UnitConcentrationT( + UnitT("ppb") +) # Speed units -SPEED_MILLIMETERS_PER_DAY: Final = "mm/d" -SPEED_INCHES_PER_DAY: Final = "in/d" -SPEED_METERS_PER_SECOND: Final = "m/s" -SPEED_INCHES_PER_HOUR: Final = "in/h" -SPEED_KILOMETERS_PER_HOUR: Final = "km/h" -SPEED_MILES_PER_HOUR: Final = "mph" +UnitSpeedT = NewType("UnitSpeedT", UnitT) +SPEED_MILLIMETERS_PER_DAY: Final[UnitSpeedT] = UnitSpeedT(UnitT("mm/d")) +SPEED_INCHES_PER_DAY: Final[UnitSpeedT] = UnitSpeedT(UnitT("in/d")) +SPEED_METERS_PER_SECOND: Final[UnitSpeedT] = UnitSpeedT(UnitT("m/s")) +SPEED_INCHES_PER_HOUR: Final[UnitSpeedT] = UnitSpeedT(UnitT("in/h")) +SPEED_KILOMETERS_PER_HOUR: Final[UnitSpeedT] = UnitSpeedT(UnitT("km/h")) +SPEED_MILES_PER_HOUR: Final[UnitSpeedT] = UnitSpeedT(UnitT("mph")) # Signal_strength units -SIGNAL_STRENGTH_DECIBELS: Final = "dB" -SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" +UnitSignalStrengthT = NewType("UnitSignalStrengthT", UnitT) +SIGNAL_STRENGTH_DECIBELS: Final[UnitSignalStrengthT] = UnitSignalStrengthT(UnitT("dB")) +SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final[UnitSignalStrengthT] = UnitSignalStrengthT( + UnitT("dBm") +) # Data units -DATA_BITS: Final = "bit" -DATA_KILOBITS: Final = "kbit" -DATA_MEGABITS: Final = "Mbit" -DATA_GIGABITS: Final = "Gbit" -DATA_BYTES: Final = "B" -DATA_KILOBYTES: Final = "kB" -DATA_MEGABYTES: Final = "MB" -DATA_GIGABYTES: Final = "GB" -DATA_TERABYTES: Final = "TB" -DATA_PETABYTES: Final = "PB" -DATA_EXABYTES: Final = "EB" -DATA_ZETTABYTES: Final = "ZB" -DATA_YOTTABYTES: Final = "YB" -DATA_KIBIBYTES: Final = "KiB" -DATA_MEBIBYTES: Final = "MiB" -DATA_GIBIBYTES: Final = "GiB" -DATA_TEBIBYTES: Final = "TiB" -DATA_PEBIBYTES: Final = "PiB" -DATA_EXBIBYTES: Final = "EiB" -DATA_ZEBIBYTES: Final = "ZiB" -DATA_YOBIBYTES: Final = "YiB" -DATA_RATE_BITS_PER_SECOND: Final = "bit/s" -DATA_RATE_KILOBITS_PER_SECOND: Final = "kbit/s" -DATA_RATE_MEGABITS_PER_SECOND: Final = "Mbit/s" -DATA_RATE_GIGABITS_PER_SECOND: Final = "Gbit/s" -DATA_RATE_BYTES_PER_SECOND: Final = "B/s" -DATA_RATE_KILOBYTES_PER_SECOND: Final = "kB/s" -DATA_RATE_MEGABYTES_PER_SECOND: Final = "MB/s" -DATA_RATE_GIGABYTES_PER_SECOND: Final = "GB/s" -DATA_RATE_KIBIBYTES_PER_SECOND: Final = "KiB/s" -DATA_RATE_MEBIBYTES_PER_SECOND: Final = "MiB/s" -DATA_RATE_GIBIBYTES_PER_SECOND: Final = "GiB/s" +UnitDataT = NewType("UnitDataT", UnitT) +DATA_BITS: Final[UnitDataT] = UnitDataT(UnitT("bit")) +DATA_KILOBITS: Final[UnitDataT] = UnitDataT(UnitT("kbit")) +DATA_MEGABITS: Final[UnitDataT] = UnitDataT(UnitT("Mbit")) +DATA_GIGABITS: Final[UnitDataT] = UnitDataT(UnitT("Gbit")) +DATA_BYTES: Final[UnitDataT] = UnitDataT(UnitT("B")) +DATA_KILOBYTES: Final[UnitDataT] = UnitDataT(UnitT("kB")) +DATA_MEGABYTES: Final[UnitDataT] = UnitDataT(UnitT("MB")) +DATA_GIGABYTES: Final[UnitDataT] = UnitDataT(UnitT("GB")) +DATA_TERABYTES: Final[UnitDataT] = UnitDataT(UnitT("TB")) +DATA_PETABYTES: Final[UnitDataT] = UnitDataT(UnitT("PB")) +DATA_EXABYTES: Final[UnitDataT] = UnitDataT(UnitT("EB")) +DATA_ZETTABYTES: Final[UnitDataT] = UnitDataT(UnitT("ZB")) +DATA_YOTTABYTES: Final[UnitDataT] = UnitDataT(UnitT("YB")) +DATA_KIBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("KiB")) +DATA_MEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("MiB")) +DATA_GIBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("GiB")) +DATA_TEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("TiB")) +DATA_PEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("PiB")) +DATA_EXBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("EiB")) +DATA_ZEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("ZiB")) +DATA_YOBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("YiB")) + +# Data_rate units +UnitDataRateT = NewType("UnitDataRateT", UnitT) +DATA_RATE_BITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("bit/s")) +DATA_RATE_KILOBITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("kbit/s")) +DATA_RATE_MEGABITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("Mbit/s")) +DATA_RATE_GIGABITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("Gbit/s")) +DATA_RATE_BYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("B/s")) +DATA_RATE_KILOBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("kB/s")) +DATA_RATE_MEGABYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("MB/s")) +DATA_RATE_GIGABYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("GB/s")) +DATA_RATE_KIBIBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("KiB/s")) +DATA_RATE_MEBIBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("MiB/s")) +DATA_RATE_GIBIBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("GiB/s")) + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP: Final = "stop" @@ -653,13 +699,14 @@ RESTART_EXIT_CODE: Final = 100 UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit." -LENGTH: Final = "length" -MASS: Final = "mass" -PRESSURE: Final = "pressure" -VOLUME: Final = "volume" -TEMPERATURE: Final = "temperature" -SPEED_MS: Final = "speed_ms" -ILLUMINANCE: Final = "illuminance" +UnitTypeT = NewType("UnitTypeT", str) +LENGTH: Final[UnitTypeT] = UnitTypeT("length") +MASS: Final[UnitTypeT] = UnitTypeT("mass") +PRESSURE: Final[UnitTypeT] = UnitTypeT("pressure") +VOLUME: Final[UnitTypeT] = UnitTypeT("volume") +TEMPERATURE: Final[UnitTypeT] = UnitTypeT("temperature") +SPEED_MS: Final[UnitTypeT] = UnitTypeT("speed_ms") +ILLUMINANCE: Final[UnitTypeT] = UnitTypeT("illuminance") WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index e0f089e93b9..2693fc13fc2 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -3,13 +3,16 @@ from __future__ import annotations from numbers import Number -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitTemperatureT from homeassistant.core import HomeAssistant from homeassistant.util.temperature import convert as convert_temperature def display_temp( - hass: HomeAssistant, temperature: float | None, unit: str, precision: float + hass: HomeAssistant, + temperature: float | None, + unit: UnitTemperatureT, + precision: float, ) -> float | None: """Convert temperature into preferred units/precision for display.""" temperature_unit = unit diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 592c7c3145e..921e2941760 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -15,9 +15,10 @@ from homeassistant.const import ( LENGTH_MILLIMETERS, LENGTH_YARD, UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitLengthT, ) -VALID_UNITS = [ +VALID_UNITS: tuple[UnitLengthT, ...] = ( LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_FEET, @@ -26,9 +27,9 @@ VALID_UNITS = [ LENGTH_MILLIMETERS, LENGTH_INCHES, LENGTH_YARD, -] +) -TO_METERS: dict[str, Callable[[float], float]] = { +TO_METERS: dict[UnitLengthT, Callable[[float], float]] = { LENGTH_METERS: lambda meters: meters, LENGTH_MILES: lambda miles: miles * 1609.344, LENGTH_YARD: lambda yards: yards * 0.9144, @@ -39,7 +40,7 @@ TO_METERS: dict[str, Callable[[float], float]] = { LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001, } -METERS_TO: dict[str, Callable[[float], float]] = { +METERS_TO: dict[UnitLengthT, Callable[[float], float]] = { LENGTH_METERS: lambda meters: meters, LENGTH_MILES: lambda meters: meters * 0.000621371, LENGTH_YARD: lambda meters: meters * 1.09361, @@ -51,7 +52,7 @@ METERS_TO: dict[str, Callable[[float], float]] = { } -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, unit_1: UnitLengthT, unit_2: UnitLengthT) -> float: """Convert one unit of measurement to another.""" if unit_1 not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, LENGTH)) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 24ad3242921..9939b02e141 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -1,4 +1,6 @@ """Pressure util functions.""" +from __future__ import annotations + from numbers import Number from homeassistant.const import ( @@ -9,11 +11,18 @@ from homeassistant.const import ( PRESSURE_PA, PRESSURE_PSI, UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitPressureT, ) -VALID_UNITS = [PRESSURE_PA, PRESSURE_HPA, PRESSURE_MBAR, PRESSURE_INHG, PRESSURE_PSI] +VALID_UNITS: tuple[UnitPressureT, ...] = ( + PRESSURE_PA, + PRESSURE_HPA, + PRESSURE_MBAR, + PRESSURE_INHG, + PRESSURE_PSI, +) -UNIT_CONVERSION = { +UNIT_CONVERSION: dict[UnitPressureT, float] = { PRESSURE_PA: 1, PRESSURE_HPA: 1 / 100, PRESSURE_MBAR: 1 / 100, @@ -22,7 +31,7 @@ UNIT_CONVERSION = { } -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, unit_1: UnitPressureT, unit_2: UnitPressureT) -> float: """Convert one unit of measurement to another.""" if unit_1 not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, PRESSURE)) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index bc3cb4c1017..015f33383d1 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -5,6 +5,7 @@ from homeassistant.const import ( TEMP_KELVIN, TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitTemperatureT, ) @@ -37,7 +38,10 @@ def celsius_to_kelvin(celsius: float, interval: bool = False) -> float: def convert( - temperature: float, from_unit: str, to_unit: str, interval: bool = False + temperature: float, + from_unit: UnitTemperatureT, + to_unit: UnitTemperatureT, + interval: bool = False, ) -> float: """Convert a temperature from one unit to another.""" if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN): diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index b5c8c38425a..e49c1895180 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -24,6 +24,13 @@ from homeassistant.const import ( VOLUME, VOLUME_GALLONS, VOLUME_LITERS, + UnitLengthT, + UnitMassT, + UnitPressureT, + UnitT, + UnitTemperatureT, + UnitTypeT, + UnitVolumeT, ) from homeassistant.util import ( distance as distance_util, @@ -36,17 +43,23 @@ from homeassistant.util import ( LENGTH_UNITS = distance_util.VALID_UNITS -MASS_UNITS = [MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS] +MASS_UNITS: tuple[UnitMassT, ...] = ( + MASS_POUNDS, + MASS_OUNCES, + MASS_KILOGRAMS, + MASS_GRAMS, +) PRESSURE_UNITS = pressure_util.VALID_UNITS VOLUME_UNITS = volume_util.VALID_UNITS -TEMPERATURE_UNITS = [TEMP_FAHRENHEIT, TEMP_CELSIUS] +TEMPERATURE_UNITS: tuple[UnitTemperatureT, ...] = (TEMP_FAHRENHEIT, TEMP_CELSIUS) -def is_valid_unit(unit: str, unit_type: str) -> bool: +def is_valid_unit(unit: UnitT, unit_type: UnitTypeT) -> bool: """Check if the unit is valid for it's type.""" + units: tuple[UnitT, ...] if unit_type == LENGTH: units = LENGTH_UNITS elif unit_type == TEMPERATURE: @@ -69,11 +82,11 @@ class UnitSystem: def __init__( self, name: str, - temperature: str, - length: str, - volume: str, - mass: str, - pressure: str, + temperature: UnitTemperatureT, + length: UnitLengthT, + volume: UnitVolumeT, + mass: UnitMassT, + pressure: UnitPressureT, ) -> None: """Initialize the unit system object.""" errors: str = ", ".join( @@ -103,14 +116,14 @@ class UnitSystem: """Determine if this is the metric unit system.""" return self.name == CONF_UNIT_SYSTEM_METRIC - def temperature(self, temperature: float, from_unit: str) -> float: + def temperature(self, temperature: float, from_unit: UnitTemperatureT) -> float: """Convert the given temperature to this unit system.""" if not isinstance(temperature, Number): raise TypeError(f"{temperature!s} is not a numeric value.") return temperature_util.convert(temperature, from_unit, self.temperature_unit) - def length(self, length: float | None, from_unit: str) -> float: + def length(self, length: float | None, from_unit: UnitLengthT) -> float: """Convert the given length to this unit system.""" if not isinstance(length, Number): raise TypeError(f"{length!s} is not a numeric value.") @@ -120,7 +133,7 @@ class UnitSystem: length, from_unit, self.length_unit ) - def pressure(self, pressure: float | None, from_unit: str) -> float: + def pressure(self, pressure: float | None, from_unit: UnitPressureT) -> float: """Convert the given pressure to this unit system.""" if not isinstance(pressure, Number): raise TypeError(f"{pressure!s} is not a numeric value.") @@ -130,7 +143,7 @@ class UnitSystem: pressure, from_unit, self.pressure_unit ) - def volume(self, volume: float | None, from_unit: str) -> float: + def volume(self, volume: float | None, from_unit: UnitVolumeT) -> float: """Convert the given volume to this unit system.""" if not isinstance(volume, Number): raise TypeError(f"{volume!s} is not a numeric value.") @@ -138,7 +151,7 @@ class UnitSystem: # type ignore: https://github.com/python/mypy/issues/7207 return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore - def as_dict(self) -> dict[str, str]: + def as_dict(self) -> dict[str, UnitT]: """Convert the unit system to a dictionary.""" return { LENGTH: self.length_unit, diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 5d94f848cd8..ddf2be5a3bc 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -1,4 +1,6 @@ """Volume conversion util functions.""" +from __future__ import annotations + from numbers import Number from homeassistant.const import ( @@ -8,9 +10,15 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, + UnitVolumeT, ) -VALID_UNITS = [VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE] +VALID_UNITS: tuple[UnitVolumeT, ...] = ( + VOLUME_LITERS, + VOLUME_MILLILITERS, + VOLUME_GALLONS, + VOLUME_FLUID_OUNCE, +) def __liter_to_gallon(liter: float) -> float: @@ -23,7 +31,7 @@ def __gallon_to_liter(gallon: float) -> float: return gallon * 3.785 -def convert(volume: float, from_unit: str, to_unit: str) -> float: +def convert(volume: float, from_unit: UnitVolumeT, to_unit: UnitVolumeT) -> float: """Convert a temperature from one unit to another.""" if from_unit not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, VOLUME)) From 61056afe0d6fe6fcd4bf6590f715b41df45738c2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 18 Jul 2021 19:00:02 +0200 Subject: [PATCH 337/818] Upgrade pysonos to 0.0.53 (#53137) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e873b43839a..a35faee4ad6 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.52"], + "requirements": ["pysonos==0.0.53"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index b5de00118ed..ea0bf4bac5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1768,7 +1768,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.52 +pysonos==0.0.53 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d9f7ee19ee..09026db6433 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.52 +pysonos==0.0.53 # homeassistant.components.spc pyspcwebgw==0.4.0 From 6c05e2746d77b750d0059b956fe4a9da6e780d5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 09:27:25 -1000 Subject: [PATCH 338/818] Improve error message when HomeKit does not support an entity (#53129) --- homeassistant/components/homekit/__init__.py | 7 +++++ tests/components/homekit/test_homekit.py | 30 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 842bcf81efe..5abc9adb9ca 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -679,6 +679,13 @@ class HomeKit: state = entity_states[0] conf = self._config.pop(state.entity_id, {}) acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) + if acc is None: + _LOGGER.error( + "HomeKit %s cannot startup: entity not supported: %s", + self._name, + self._filter.config, + ) + return False else: self.bridge = HomeBridge(self.hass, self.driver, self._name) for state in entity_states: diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 7fd588e62d0..235c3027c98 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1207,6 +1207,36 @@ async def test_homekit_start_in_accessory_mode( assert homekit.status == STATUS_RUNNING +async def test_homekit_start_in_accessory_mode_unsupported_entity( + hass, hk_driver, mock_zeroconf, device_reg, caplog +): + """Test HomeKit start method in accessory mode with an unsupported entity.""" + entry = await async_init_integration(hass) + + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + + homekit.bridge = Mock() + homekit.bridge.accessories = [] + homekit.driver = hk_driver + homekit.driver.accessory = Accessory(hk_driver, "any") + + hass.states.async_set("notsupported.demo", "on") + + with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( + f"{PATH_HOMEKIT}.show_setup_message" + ) as mock_setup_msg, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ) as hk_driver_start: + await homekit.async_start() + + await hass.async_block_till_done() + assert not mock_add_acc.called + assert not mock_setup_msg.called + assert not hk_driver_start.called + assert homekit.status == STATUS_WAIT + assert "entity not supported" in caplog.text + + async def test_homekit_start_in_accessory_mode_missing_entity( hass, hk_driver, mock_zeroconf, device_reg, caplog ): From 0804959f118a8402dfbc0b28041728c7ead30ffb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 09:37:34 -1000 Subject: [PATCH 339/818] Bump nexia to 0.9.10 to fix asair login (#53122) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index eb471597ec6..a453ec7f1df 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.9"], + "requirements": ["nexia==0.9.10"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index ea0bf4bac5a..2fa83e6aec3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1015,7 +1015,7 @@ nettigo-air-monitor==1.0.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.9 +nexia==0.9.10 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09026db6433..3432768ce67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -569,7 +569,7 @@ netdisco==2.9.0 nettigo-air-monitor==1.0.0 # homeassistant.components.nexia -nexia==0.9.9 +nexia==0.9.10 # homeassistant.components.notify_events notify-events==1.0.4 From 56d66434b392130cb60c56d1ebc26b1138f2240d Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sun, 18 Jul 2021 15:51:02 -0400 Subject: [PATCH 340/818] Bump greeclimate to 0.11.8 (#53148) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 8108df18cc8..e62bf402523 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.11.7"], + "requirements": ["greeclimate==0.11.8"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2fa83e6aec3..eb56cfb246f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -712,7 +712,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.11.7 +greeclimate==0.11.8 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3432768ce67..c7202dbd0c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -400,7 +400,7 @@ google-nest-sdm==0.2.12 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.11.7 +greeclimate==0.11.8 # homeassistant.components.growatt_server growattServer==1.0.1 From fe22d5a6753c2581fd007fb5598a63ba6090cb85 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 15:54:43 -0400 Subject: [PATCH 341/818] Fix home_connect test coverage (#53086) * Fix home_connect test coverage * remove from hassfest exclusions --- .coveragerc | 8 +++++++- script/hassfest/coverage.py | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6f6adee207a..9c1ef3154b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -427,7 +427,13 @@ omit = homeassistant/components/hive/water_heater.py homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/switch.py - homeassistant/components/home_connect/* + homeassistant/components/home_connect/__init__.py + homeassistant/components/home_connect/api.py + homeassistant/components/home_connect/binary_sensor.py + homeassistant/components/home_connect/entity.py + homeassistant/components/home_connect/light.py + homeassistant/components/home_connect/sensor.py + homeassistant/components/home_connect/switch.py homeassistant/components/homematic/* homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index a8570283f57..dd4b807d5c0 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -31,7 +31,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("hangouts", "config_flow.py"), ("harmony", "config_flow.py"), ("hisense_aehw4a1", "config_flow.py"), - ("home_connect", "config_flow.py"), ("huawei_lte", "config_flow.py"), ("ifttt", "config_flow.py"), ("ios", "config_flow.py"), From 284e13464d6c253d035a450f4456a3490f6ce007 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 15:56:54 -0400 Subject: [PATCH 342/818] Fix home plus control coverage (#53087) * Fix home_plus_control test coverage * add back api, switch --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 9c1ef3154b3..7c8954a5f80 100644 --- a/.coveragerc +++ b/.coveragerc @@ -439,7 +439,6 @@ omit = homeassistant/components/homematic/cover.py homeassistant/components/homematic/notify.py homeassistant/components/home_plus_control/api.py - homeassistant/components/home_plus_control/helpers.py homeassistant/components/home_plus_control/switch.py homeassistant/components/homeworks/* homeassistant/components/honeywell/climate.py From cb1eab6c24779ce71ae4d5d76fc7ed038532c9d6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 16:10:42 -0400 Subject: [PATCH 343/818] Use entity class attributes for bosch_shc (#53057) --- .../components/bosch_shc/binary_sensor.py | 44 ++-- homeassistant/components/bosch_shc/entity.py | 53 ++-- homeassistant/components/bosch_shc/sensor.py | 227 ++++++------------ 3 files changed, 109 insertions(+), 215 deletions(-) diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index d2c2df838c5..29e45ba2359 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -1,5 +1,6 @@ """Platform for binarysensor integration.""" from boschshcpy import SHCBatteryDevice, SHCSession, SHCShutterContact +from boschshcpy.device import SHCDevice from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY, @@ -50,37 +51,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class ShutterContactSensor(SHCEntity, BinarySensorEntity): - """Representation of a SHC shutter contact sensor.""" + """Representation of an SHC shutter contact sensor.""" - @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.state == SHCShutterContact.ShutterContactService.State.OPEN - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC shutter contact sensor..""" + super().__init__(device, parent_id, entry_id) switcher = { "ENTRANCE_DOOR": DEVICE_CLASS_DOOR, "REGULAR_WINDOW": DEVICE_CLASS_WINDOW, "FRENCH_WINDOW": DEVICE_CLASS_DOOR, "GENERIC": DEVICE_CLASS_WINDOW, } - return switcher.get(self._device.device_class, DEVICE_CLASS_WINDOW) + self._attr_device_class = switcher.get( + self._device.device_class, DEVICE_CLASS_WINDOW + ) + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._device.state == SHCShutterContact.ShutterContactService.State.OPEN class BatterySensor(SHCEntity, BinarySensorEntity): - """Representation of a SHC battery reporting sensor.""" + """Representation of an SHC battery reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_battery" + _attr_device_class = DEVICE_CLASS_BATTERY - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Battery" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC battery reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Battery" + self._attr_unique_id = f"{device.serial}_battery" @property def is_on(self): @@ -88,8 +89,3 @@ class BatterySensor(SHCEntity, BinarySensorEntity): return ( self._device.batterylevel != SHCBatteryDevice.BatteryLevelService.State.OK ) - - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS_BATTERY diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index d693b0cdfcc..a8966ce2f4a 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -20,11 +20,26 @@ async def async_remove_devices(hass, entity, entry_id): class SHCEntity(Entity): """Representation of a SHC base entity.""" + _attr_should_poll = False + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize the generic SHC device.""" self._device = device - self._parent_id = parent_id self._entry_id = entry_id + self._attr_name = device.name + self._attr_unique_id = device.serial + self._attr_device_info = { + "identifiers": {(DOMAIN, device.id)}, + "name": device.name, + "manufacturer": device.manufacturer, + "model": device.device_model, + "via_device": ( + DOMAIN, + device.parent_device_id + if device.parent_device_id is not None + else parent_id, + ), + } async def async_added_to_hass(self): """Subscribe to SHC events.""" @@ -50,43 +65,7 @@ class SHCEntity(Entity): service.unsubscribe_callback(self.entity_id) self._device.unsubscribe_callback(self.entity_id) - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.serial - - @property - def name(self): - """Name of the entity.""" - return self._device.name - - @property - def device_id(self): - """Device id of the entity.""" - return self._device.id - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self._device.name, - "manufacturer": self._device.manufacturer, - "model": self._device.device_model, - "via_device": ( - DOMAIN, - self._device.parent_device_id - if self._device.parent_device_id is not None - else self._parent_id, - ), - } - @property def available(self): """Return false if status is unavailable.""" return self._device.status == "AVAILABLE" - - @property - def should_poll(self): - """Report polling mode. SHC Entity is communicating via long polling.""" - return False diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 9f3cf2d5bc3..55aa1eb5772 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -1,5 +1,6 @@ """Platform for sensor integration.""" from boschshcpy import SHCSession +from boschshcpy.device import SHCDevice from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -143,104 +144,67 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TemperatureSensor(SHCEntity, SensorEntity): - """Representation of a SHC temperature reporting sensor.""" + """Representation of an SHC temperature reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_temperature" + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Temperature" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC temperature reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Temperature" + self._attr_unique_id = f"{device.serial}_temperature" @property def state(self): """Return the state of the sensor.""" return self._device.temperature - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return TEMP_CELSIUS - - @property - def device_class(self): - """Return the class of this device.""" - return DEVICE_CLASS_TEMPERATURE - class HumiditySensor(SHCEntity, SensorEntity): - """Representation of a SHC humidity reporting sensor.""" + """Representation of an SHC humidity reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_humidity" + _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_unit_of_measurement = PERCENTAGE - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Humidity" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC humidity reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Humidity" + self._attr_unique_id = f"{device.serial}_humidity" @property def state(self): """Return the state of the sensor.""" return self._device.humidity - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return PERCENTAGE - - @property - def device_class(self): - """Return the class of this device.""" - return DEVICE_CLASS_HUMIDITY - class PuritySensor(SHCEntity, SensorEntity): - """Representation of a SHC purity reporting sensor.""" + """Representation of an SHC purity reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_purity" + _attr_icon = "mdi:molecule-co2" + _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Purity" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC purity reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Purity" + self._attr_unique_id = f"{device.serial}_purity" @property def state(self): """Return the state of the sensor.""" return self._device.purity - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return CONCENTRATION_PARTS_PER_MILLION - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:molecule-co2" - class AirQualitySensor(SHCEntity, SensorEntity): - """Representation of a SHC airquality reporting sensor.""" + """Representation of an SHC airquality reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_airquality" - - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Air Quality" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC airquality reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Air Quality" + self._attr_unique_id = f"{device.serial}_airquality" @property def state(self): @@ -256,17 +220,13 @@ class AirQualitySensor(SHCEntity, SensorEntity): class TemperatureRatingSensor(SHCEntity, SensorEntity): - """Representation of a SHC temperature rating sensor.""" + """Representation of an SHC temperature rating sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_temperature_rating" - - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Temperature Rating" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC temperature rating sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Temperature Rating" + self._attr_unique_id = f"{device.serial}_temperature_rating" @property def state(self): @@ -275,17 +235,13 @@ class TemperatureRatingSensor(SHCEntity, SensorEntity): class HumidityRatingSensor(SHCEntity, SensorEntity): - """Representation of a SHC humidity rating sensor.""" + """Representation of an SHC humidity rating sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_humidity_rating" - - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Humidity Rating" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC humidity rating sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Humidity Rating" + self._attr_unique_id = f"{device.serial}_humidity_rating" @property def state(self): @@ -294,17 +250,13 @@ class HumidityRatingSensor(SHCEntity, SensorEntity): class PurityRatingSensor(SHCEntity, SensorEntity): - """Representation of a SHC purity rating sensor.""" + """Representation of an SHC purity rating sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_purity_rating" - - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Purity Rating" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC purity rating sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Purity Rating" + self._attr_unique_id = f"{device.serial}_purity_rating" @property def state(self): @@ -313,91 +265,58 @@ class PurityRatingSensor(SHCEntity, SensorEntity): class PowerSensor(SHCEntity, SensorEntity): - """Representation of a SHC power reporting sensor.""" + """Representation of an SHC power reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_power" + _attr_device_class = DEVICE_CLASS_POWER + _attr_unit_of_measurement = POWER_WATT - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Power" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC power reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Power" + self._attr_unique_id = f"{device.serial}_power" @property def state(self): """Return the state of the sensor.""" return self._device.powerconsumption - @property - def device_class(self): - """Return the class of this device.""" - return DEVICE_CLASS_POWER - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return POWER_WATT - class EnergySensor(SHCEntity, SensorEntity): - """Representation of a SHC energy reporting sensor.""" + """Representation of an SHC energy reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_energy" + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Energy" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC energy reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{self._device.name} Energy" + self._attr_unique_id = f"{self._device.serial}_energy" @property def state(self): """Return the state of the sensor.""" return self._device.energyconsumption / 1000.0 - @property - def device_class(self): - """Return the class of this device.""" - return DEVICE_CLASS_ENERGY - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return ENERGY_KILO_WATT_HOUR - class ValveTappetSensor(SHCEntity, SensorEntity): - """Representation of a SHC valve tappet reporting sensor.""" + """Representation of an SHC valve tappet reporting sensor.""" - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device.serial}_valvetappet" + _attr_icon = "mdi:gauge" + _attr_unit_of_measurement = PERCENTAGE - @property - def name(self): - """Return the name of this sensor.""" - return f"{self._device.name} Valvetappet" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC valve tappet reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Valvetappet" + self._attr_unique_id = f"{device.serial}_valvetappet" @property def state(self): """Return the state of the sensor.""" return self._device.position - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:gauge" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return PERCENTAGE - @property def extra_state_attributes(self): """Return the state attributes.""" From 236738c455b827413ba052b1ea3bb24214d9f5db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 10:17:58 -1000 Subject: [PATCH 344/818] Add support for tilt only covers to HomeKit (#53130) --- .../components/homekit/accessories.py | 2 +- .../components/homekit/type_covers.py | 19 ++++++- .../homekit/test_get_accessories.py | 20 ++++++- tests/components/homekit/test_type_covers.py | 57 +++++++++++++++++-- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index b9bd62246cf..3c843955222 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -127,7 +127,7 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 and features & cover.SUPPORT_SET_POSITION ): a_type = "Window" - elif features & cover.SUPPORT_SET_POSITION: + elif features & (cover.SUPPORT_SET_POSITION | cover.SUPPORT_SET_TILT_POSITION): a_type = "WindowCovering" elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = "WindowCoveringBasic" diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index f21287b3bf8..099eced62d3 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -13,6 +13,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, + SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, ) @@ -53,6 +54,8 @@ from .const import ( HK_POSITION_GOING_TO_MAX, HK_POSITION_GOING_TO_MIN, HK_POSITION_STOPPED, + PROP_MAX_VALUE, + PROP_MIN_VALUE, SERV_GARAGE_DOOR_OPENER, SERV_WINDOW, SERV_WINDOW_COVERING, @@ -273,12 +276,24 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=category, service=service) state = self.hass.states.get(self.entity_id) - self.char_current_position = self.serv_cover.configure_char( CHAR_CURRENT_POSITION, value=0 ) + target_args = {"value": 0} + if self.features & SUPPORT_SET_POSITION: + target_args["setter_callback"] = self.move_cover + else: + # If its tilt only we lock the position state to 0 (closed) + # since CHAR_CURRENT_POSITION/CHAR_TARGET_POSITION are required + # by homekit, but really don't exist. + _LOGGER.debug( + "%s does not support setting position, current position will be locked to closed", + self.entity_id, + ) + target_args["properties"] = {PROP_MIN_VALUE: 0, PROP_MAX_VALUE: 0} + self.char_target_position = self.serv_cover.configure_char( - CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover + CHAR_TARGET_POSITION, **target_args ) self.char_position_state = self.serv_cover.configure_char( CHAR_POSITION_STATE, value=HK_POSITION_STOPPED diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 491d686162d..1b220153195 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -126,14 +126,28 @@ def test_types(type_name, entity_id, state, attrs, config): "Window", "cover.set_position", "open", - {ATTR_DEVICE_CLASS: "window", ATTR_SUPPORTED_FEATURES: 4}, + { + ATTR_DEVICE_CLASS: "window", + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + }, + ), + ( + "WindowCovering", + "cover.set_position", + "open", + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION}, + ), + ( + "WindowCovering", + "cover.tilt", + "open", + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_TILT_POSITION}, ), - ("WindowCovering", "cover.set_position", "open", {ATTR_SUPPORTED_FEATURES: 4}), ( "WindowCoveringBasic", "cover.open_window", "open", - {ATTR_SUPPORTED_FEATURES: 3}, + {ATTR_SUPPORTED_FEATURES: (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE)}, ), ], ) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8514801687c..89407edfbef 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -18,6 +18,8 @@ from homeassistant.components.homekit.const import ( HK_DOOR_CLOSING, HK_DOOR_OPEN, HK_DOOR_OPENING, + PROP_MAX_VALUE, + PROP_MIN_VALUE, ) from homeassistant.components.homekit.type_covers import ( GarageDoorOpener, @@ -133,7 +135,9 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" - hass.states.async_set(entity_id, None) + hass.states.async_set( + entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION} + ) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run() @@ -145,31 +149,51 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 - hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_POSITION: None}) + hass.states.async_set( + entity_id, + STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: None}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 2 - hass.states.async_set(entity_id, STATE_OPENING, {ATTR_CURRENT_POSITION: 60}) + hass.states.async_set( + entity_id, + STATE_OPENING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 60}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 60 assert acc.char_target_position.value == 60 assert acc.char_position_state.value == 1 - hass.states.async_set(entity_id, STATE_OPENING, {ATTR_CURRENT_POSITION: 70.0}) + hass.states.async_set( + entity_id, + STATE_OPENING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 70.0}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 70 assert acc.char_target_position.value == 70 assert acc.char_position_state.value == 1 - hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_POSITION: 50}) + hass.states.async_set( + entity_id, + STATE_CLOSING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 50 assert acc.char_position_state.value == 0 - hass.states.async_set(entity_id, STATE_OPEN, {ATTR_CURRENT_POSITION: 50}) + hass.states.async_set( + entity_id, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 50 @@ -283,6 +307,27 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == 75 +async def test_windowcovering_tilt_only(hass, hk_driver, events): + """Test we lock the window covering closed when its tilt only.""" + entity_id = "cover.window" + + hass.states.async_set( + entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} + ) + await hass.async_block_till_done() + acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering + + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_target_position.properties[PROP_MIN_VALUE] == 0 + assert acc.char_target_position.properties[PROP_MAX_VALUE] == 0 + + async def test_windowcovering_open_close(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" From a8ea214f2ec9366397103704a89ab250babef30f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 18 Jul 2021 15:12:05 -0600 Subject: [PATCH 345/818] Bump simplisafe-python to 11.0.2 (#53121) * Bump simplisafe-python to 11.0.2 * Fix CI --- homeassistant/components/simplisafe/__init__.py | 8 ++++++-- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 14fc4bf9a5a..b3f246b1847 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,7 +3,11 @@ import asyncio from uuid import UUID from simplipy import get_api -from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError +from simplipy.errors import ( + EndpointUnavailableError, + InvalidCredentialsError, + SimplipyError, +) import voluptuous as vol from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME @@ -372,7 +376,7 @@ class SimpliSafe: if isinstance(result, InvalidCredentialsError): raise ConfigEntryAuthFailed("Invalid credentials") from result - if isinstance(result, EndpointUnavailable): + if isinstance(result, EndpointUnavailableError): # In case the user attempts an action not allowed in their current plan, # we merely log that message at INFO level (so the user is aware, # but not spammed with ERROR messages that they cannot change): diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 02713b106bd..3ec1e38ad4d 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.1"], + "requirements": ["simplisafe-python==11.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index eb56cfb246f..82e96b1e319 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2097,7 +2097,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.1 +simplisafe-python==11.0.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7202dbd0c1..009f3dd5a37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1143,7 +1143,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.1 +simplisafe-python==11.0.2 # homeassistant.components.slack slackclient==2.5.0 From bacb6c6b14a0b25e94805bbf2af3a7bc9fe4116d Mon Sep 17 00:00:00 2001 From: jgriff2 Date: Sun, 18 Jul 2021 14:13:13 -0700 Subject: [PATCH 346/818] Fix remote rpi gpio input type (#53108) * Fix issue #45770 - Change sensor from Button to DigitalInput * Change references from button to sensor --- .../components/remote_rpi_gpio/__init__.py | 8 ++++---- .../components/remote_rpi_gpio/binary_sensor.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index afa60e44bed..b11625eb7b2 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -1,5 +1,5 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" -from gpiozero import LED, Button +from gpiozero import LED, DigitalInputDevice from gpiozero.pins.pigpio import PiGPIOFactory CONF_BOUNCETIME = "bouncetime" @@ -38,7 +38,7 @@ def setup_input(address, port, pull_mode, bouncetime): pull_gpio_up = False try: - return Button( + return DigitalInputDevice( port, pull_up=pull_gpio_up, bounce_time=bouncetime, @@ -56,6 +56,6 @@ def write_output(switch, value): switch.off() -def read_input(button): +def read_input(sensor): """Read a value from a GPIO.""" - return button.is_pressed + return sensor.value diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 86966ec4d87..aeff838d68d 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -42,12 +42,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for port_num, port_name in ports.items(): try: - button = remote_rpi_gpio.setup_input( + remote_sensor = remote_rpi_gpio.setup_input( address, port_num, pull_mode, bouncetime ) except (ValueError, IndexError, KeyError, OSError): return - new_sensor = RemoteRPiGPIOBinarySensor(port_name, button, invert_logic) + new_sensor = RemoteRPiGPIOBinarySensor(port_name, remote_sensor, invert_logic) devices.append(new_sensor) add_entities(devices, True) @@ -56,23 +56,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class RemoteRPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses a Remote Raspberry Pi GPIO.""" - def __init__(self, name, button, invert_logic): + def __init__(self, name, sensor, invert_logic): """Initialize the RPi binary sensor.""" self._name = name self._invert_logic = invert_logic self._state = False - self._button = button + self._sensor = sensor async def async_added_to_hass(self): """Run when entity about to be added to hass.""" def read_gpio(): """Read state from GPIO.""" - self._state = remote_rpi_gpio.read_input(self._button) + self._state = remote_rpi_gpio.read_input(self._sensor) self.schedule_update_ha_state() - self._button.when_released = read_gpio - self._button.when_pressed = read_gpio + self._sensor.when_deactivated = read_gpio + self._sensor.when_activated = read_gpio @property def should_poll(self): @@ -97,6 +97,6 @@ class RemoteRPiGPIOBinarySensor(BinarySensorEntity): def update(self): """Update the GPIO state.""" try: - self._state = remote_rpi_gpio.read_input(self._button) + self._state = remote_rpi_gpio.read_input(self._sensor) except requests.exceptions.ConnectionError: return From 0cf95bb0c2bfbf79d6f6dd78f508ca6594cba55e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 17:19:25 -0400 Subject: [PATCH 347/818] Use entity class attributes for arwn (#52683) --- homeassistant/components/arwn/sensor.py | 63 +++++-------------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 83ff7da1b56..1c95911a19e 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -124,64 +124,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class ArwnSensor(SensorEntity): """Representation of an ARWN sensor.""" + _attr_should_poll = False + def __init__(self, topic, name, state_key, units, icon=None, device_class=None): """Initialize the sensor.""" - self.hass = None self.entity_id = _slug(name) - self._name = name + self._attr_name = name # This mqtt topic for the sensor which is its uid - self._uid = topic + self._attr_unique_id = topic self._state_key = state_key - self.event = {} - self._unit_of_measurement = units - self._icon = icon - self._device_class = device_class + self._attr_unit_of_measurement = units + self._attr_icon = icon + self._attr_device_class = device_class def set_event(self, event): """Update the sensor with the most recent event.""" - self.event = {} - self.event.update(event) + ev = {} + ev.update(event) + self._attr_extra_state_attributes = ev + self._attr_state = ev.get(self._state_key, None) self.async_write_ha_state() - - @property - def state(self): - """Return the state of the device.""" - return self.event.get(self._state_key, None) - - @property - def name(self): - """Get the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID. - - This is based on the topic that comes from mqtt - """ - return self._uid - - @property - def extra_state_attributes(self): - """Return all the state attributes.""" - return self.event - - @property - def unit_of_measurement(self): - """Return the unit of measurement the state is expressed in.""" - return self._unit_of_measurement - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - - @property - def icon(self): - """Return the icon of device based on its type.""" - return self._icon From 73976d2a2652bc2f20a4eac11edc32b7541500fb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 17:21:12 -0400 Subject: [PATCH 348/818] Use entity class attributes for Blink (#52891) * Use entity class attributes for blink * rework * revert extra state attributes --- .../components/blink/alarm_control_panel.py | 52 +++++-------------- .../components/blink/binary_sensor.py | 29 ++--------- homeassistant/components/blink/camera.py | 19 ++----- homeassistant/components/blink/sensor.py | 46 ++++------------ 4 files changed, 28 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index ed2b46acaa1..ea215ebb689 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -29,56 +29,28 @@ async def async_setup_entry(hass, config, async_add_entities): class BlinkSyncModule(AlarmControlPanelEntity): """Representation of a Blink Alarm Control Panel.""" + _attr_icon = ICON + _attr_supported_features = SUPPORT_ALARM_ARM_AWAY + def __init__(self, data, name, sync): """Initialize the alarm control panel.""" self.data = data self.sync = sync self._name = name - self._state = None - - @property - def unique_id(self): - """Return the unique id for the sync module.""" - return self.sync.serial - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_AWAY - - @property - def name(self): - """Return the name of the panel.""" - return f"{DOMAIN} {self._name}" - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attr = self.sync.attributes - attr["network_info"] = self.data.networks - attr["associated_cameras"] = list(self.sync.cameras) - attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION - return attr + self._attr_unique_id = sync.serial + self._attr_name = f"{DOMAIN} {name}" def update(self): """Update the state of the device.""" _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) self.data.refresh() - mode = self.sync.arm - if mode: - self._state = STATE_ALARM_ARMED_AWAY - else: - self._state = STATE_ALARM_DISARMED + self._attr_state = ( + STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED + ) + self.sync.attributes["network_info"] = self.data.networks + self.sync.attributes["associated_cameras"] = list(self.sync.cameras) + self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION + self._attr_extra_state_attributes = self.sync.attributes def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index f69c94f0f5e..f9b8ec31605 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -33,31 +33,10 @@ class BlinkBinarySensor(BinarySensorEntity): self.data = data self._type = sensor_type name, device_class = BINARY_SENSORS[sensor_type] - self._name = f"{DOMAIN} {camera} {name}" - self._device_class = device_class + self._attr_name = f"{DOMAIN} {camera} {name}" + self._attr_device_class = device_class self._camera = data.cameras[camera] - self._state = None - self._unique_id = f"{self._camera.serial}-{self._type}" - - @property - def name(self): - """Return the name of the blink sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return self._unique_id - - @property - def device_class(self): - """Return the class of this device.""" - return self._device_class - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state + self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" def update(self): """Update sensor state.""" @@ -65,4 +44,4 @@ class BlinkBinarySensor(BinarySensorEntity): state = self._camera.attributes[self._type] if self._type == TYPE_BATTERY: state = state != "ok" - self._state = state + self._attr_is_on = state diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 5085686494e..e2216dc8785 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -32,23 +32,10 @@ class BlinkCamera(Camera): """Initialize a camera.""" super().__init__() self.data = data - self._name = f"{DOMAIN} {name}" + self._attr_name = f"{DOMAIN} {name}" self._camera = camera - self._unique_id = f"{camera.serial}-camera" - self.response = None - self.current_image = None - self.last_image = None - _LOGGER.debug("Initialized blink camera %s", self._name) - - @property - def name(self): - """Return the camera name.""" - return self._name - - @property - def unique_id(self): - """Return the unique camera id.""" - return self._unique_id + self._attr_unique_id = f"{camera.serial}-camera" + _LOGGER.debug("Initialized blink camera %s", self.name) @property def extra_state_attributes(self): diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 1ec61900091..1f7cad3f872 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -40,51 +40,23 @@ class BlinkSensor(SensorEntity): def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" name, units, device_class = SENSORS[sensor_type] - self._name = f"{DOMAIN} {camera} {name}" - self._camera_name = name - self._type = sensor_type - self._device_class = device_class + self._attr_name = f"{DOMAIN} {camera} {name}" + self._attr_device_class = device_class self.data = data self._camera = data.cameras[camera] - self._state = None - self._unit_of_measurement = units - self._unique_id = f"{self._camera.serial}-{self._type}" - self._sensor_key = self._type - if self._type == "temperature": - self._sensor_key = "temperature_calibrated" - - @property - def name(self): - """Return the name of the camera.""" - return self._name - - @property - def unique_id(self): - """Return the unique id for the camera sensor.""" - return self._unique_id - - @property - def state(self): - """Return the camera's current state.""" - return self._state - - @property - def device_class(self): - """Return the device's class.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement + self._attr_unit_of_measurement = units + self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" + self._sensor_key = ( + "temperature_calibrated" if sensor_type == "temperature" else sensor_type + ) def update(self): """Retrieve sensor data from the camera.""" self.data.refresh() try: - self._state = self._camera.attributes[self._sensor_key] + self._attr_state = self._camera.attributes[self._sensor_key] except KeyError: - self._state = None + self._attr_state = None _LOGGER.error( "%s not a valid camera attribute. Did the API change?", self._sensor_key ) From c5fe01a466edd6b65b353452d1194b4c3121c848 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 17:21:40 -0400 Subject: [PATCH 349/818] Use entity class attributes for blinkt (#52893) * Use entity class attributes for blinkt * tweak * tweak * remove redundant properties --- homeassistant/components/blinkt/light.py | 62 ++++++------------------ 1 file changed, 14 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index bb9bbf315e4..e6a3ecd362d 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -41,77 +41,43 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinktLight(LightEntity): """Representation of a Blinkt! Light.""" + _attr_supported_features = SUPPORT_BLINKT + _attr_should_poll = False + _attr_assumed_state = True + def __init__(self, blinkt, name, index): """Initialize a Blinkt Light. Default brightness and white color. """ self._blinkt = blinkt - self._name = f"{name}_{index}" + self._attr_name = f"{name}_{index}" self._index = index - self._is_on = False - self._brightness = 255 - self._hs_color = [0, 0] - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def brightness(self): - """Read back the brightness of the light. - - Returns integer in the range of 1-255. - """ - return self._brightness - - @property - def hs_color(self): - """Read back the color of the light.""" - return self._hs_color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BLINKT - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def should_poll(self): - """Return if we should poll this device.""" - return False - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return True + self._attr_is_on = False + self._attr_brightness = 255 + self._attr_hs_color = [0, 0] def turn_on(self, **kwargs): """Instruct the light to turn on and set correct brightness & color.""" if ATTR_HS_COLOR in kwargs: - self._hs_color = kwargs[ATTR_HS_COLOR] + self._attr_hs_color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = self._brightness / 255 - rgb_color = color_util.color_hs_to_RGB(*self._hs_color) + percent_bright = self.brightness / 255 + rgb_color = color_util.color_hs_to_RGB(*self.hs_color) self._blinkt.set_pixel( self._index, rgb_color[0], rgb_color[1], rgb_color[2], percent_bright ) self._blinkt.show() - self._is_on = True + self._attr_is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Instruct the light to turn off.""" self._blinkt.set_pixel(self._index, 0, 0, 0, 0) self._blinkt.show() - self._is_on = False + self._attr_is_on = False self.schedule_update_ha_state() From cb6f9878c461eaa6bf059ee39edc25285feeb15f Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Sun, 18 Jul 2021 16:07:38 -0700 Subject: [PATCH 350/818] Update pylutron-caseta to 0.11.0 (#53160) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 36de8cab15b..e0acf31e99c 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.10.0", "aiolip==1.1.4"], + "requirements": ["pylutron-caseta==0.11.0", "aiolip==1.1.4"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 82e96b1e319..9998e58687c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1554,7 +1554,7 @@ pylitterbot==2021.3.1 pyloopenergy==0.2.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.10.0 +pylutron-caseta==0.11.0 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009f3dd5a37..6466fa8f5c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ pylitejet==0.3.0 pylitterbot==2021.3.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.10.0 +pylutron-caseta==0.11.0 # homeassistant.components.mailgun pymailgunner==1.4 From 531733da7b969a4a2444ab55b205fa0039bedcec Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jul 2021 00:09:50 +0000 Subject: [PATCH 351/818] [ci skip] Translation update --- .../components/abode/translations/hu.json | 3 +- .../accuweather/translations/hu.json | 6 ++ .../components/adguard/translations/hu.json | 7 ++ .../components/aemet/translations/hu.json | 10 +++ .../components/airly/translations/hu.json | 1 + .../components/airnow/translations/hu.json | 4 +- .../components/airvisual/translations/hu.json | 17 +++-- .../alarm_control_panel/translations/hu.json | 6 ++ .../alarmdecoder/translations/hu.json | 42 ++++++++++-- .../components/ambee/translations/hu.json | 6 +- .../ambee/translations/sensor.hu.json | 1 + .../components/apple_tv/translations/hu.json | 20 ++++++ .../components/asuswrt/translations/hu.json | 12 ++++ .../components/august/translations/hu.json | 1 + .../bmw_connected_drive/translations/hu.json | 10 +++ .../components/bosch_shc/translations/hu.json | 15 ++++- .../components/broadlink/translations/hu.json | 1 + .../buienradar/translations/hu.json | 29 ++++++++ .../components/canary/translations/hu.json | 1 + .../components/cast/translations/hu.json | 17 +++++ .../components/climacell/translations/hu.json | 5 ++ .../cloudflare/translations/hu.json | 8 +++ .../components/coinbase/translations/hu.json | 41 ++++++++++++ .../coolmaster/translations/hu.json | 3 +- .../coronavirus/translations/hu.json | 3 +- .../components/deconz/translations/hu.json | 4 +- .../components/denonavr/translations/hu.json | 9 +++ .../devolo_home_control/translations/hu.json | 13 +++- .../components/dsmr/translations/hu.json | 29 ++++++-- .../components/eafm/translations/hu.json | 7 ++ .../components/econet/translations/hu.json | 3 +- .../emulated_roku/translations/hu.json | 5 +- .../enphase_envoy/translations/hu.json | 3 +- .../components/epson/translations/hu.json | 3 +- .../components/ezviz/translations/hu.json | 52 ++++++++++++++ .../fireservicerota/translations/hu.json | 3 +- .../components/flume/translations/hu.json | 10 ++- .../forecast_solar/translations/hu.json | 22 +++++- .../freedompro/translations/hu.json | 20 ++++++ .../components/fritz/translations/hu.json | 48 ++++++++++++- .../components/fritzbox/translations/hu.json | 3 +- .../fritzbox_callmonitor/translations/hu.json | 14 ++++ .../garages_amsterdam/translations/hu.json | 11 ++- .../components/gios/translations/hu.json | 5 ++ .../components/goalzero/translations/hu.json | 8 ++- .../google_travel_time/translations/hu.json | 1 + .../growatt_server/translations/hu.json | 22 +++++- .../components/guardian/translations/hu.json | 3 + .../components/habitica/translations/hu.json | 4 +- .../components/hangouts/translations/hu.json | 2 + .../components/hassio/translations/hu.json | 1 + .../components/hive/translations/hu.json | 5 ++ .../homekit_controller/translations/hu.json | 13 ++++ .../homematicip_cloud/translations/hu.json | 1 + .../huawei_lte/translations/hu.json | 4 +- .../hvv_departures/translations/hu.json | 3 + .../components/hyperion/translations/hu.json | 24 ++++++- .../components/ialarm/translations/hu.json | 19 ++++++ .../components/insteon/translations/hu.json | 18 ++++- .../components/isy994/translations/hu.json | 8 +++ .../keenetic_ndms2/translations/hu.json | 19 +++++- .../components/kmtronic/translations/hu.json | 9 +++ .../components/kodi/translations/hu.json | 13 +++- .../kostal_plenticore/translations/hu.json | 21 ++++++ .../components/kraken/translations/hu.json | 18 +++++ .../components/litejet/translations/hu.json | 4 ++ .../lutron_caseta/translations/hu.json | 45 ++++++++++++- .../components/lyric/translations/hu.json | 7 +- .../components/mazda/translations/hu.json | 1 + .../media_player/translations/ar.json | 8 +++ .../components/met/translations/hu.json | 3 + .../met_eireann/translations/hu.json | 1 + .../meteoclimatic/translations/hu.json | 9 +++ .../components/metoffice/translations/hu.json | 1 + .../modern_forms/translations/hu.json | 10 ++- .../motion_blinds/translations/hu.json | 9 ++- .../components/motioneye/translations/hu.json | 39 +++++++++++ .../components/mqtt/translations/hu.json | 3 + .../components/mullvad/translations/hu.json | 5 ++ .../components/mutesync/translations/hu.json | 16 +++++ .../components/myq/translations/hu.json | 10 ++- .../components/mysensors/translations/hu.json | 46 ++++++++++++- .../components/nam/translations/hu.json | 24 +++++++ .../components/netatmo/translations/hu.json | 4 +- .../components/nexia/translations/hu.json | 1 + .../nightscout/translations/hu.json | 4 +- .../nmap_tracker/translations/hu.json | 40 +++++++++++ .../components/nuki/translations/hu.json | 10 +++ .../components/omnilogic/translations/hu.json | 1 + .../components/onewire/translations/hu.json | 3 +- .../components/onvif/translations/hu.json | 13 ++++ .../opentherm_gw/translations/hu.json | 2 + .../ovo_energy/translations/hu.json | 1 + .../components/ozw/translations/hu.json | 12 +++- .../philips_js/translations/hu.json | 14 ++++ .../components/picnic/translations/hu.json | 22 ++++++ .../components/plaato/translations/hu.json | 32 +++++++++ .../components/plugwise/translations/hu.json | 10 +++ .../components/point/translations/hu.json | 1 + .../progettihwsw/translations/hu.json | 11 ++- .../components/ps4/translations/hu.json | 15 ++++- .../pvpc_hourly_pricing/translations/hu.json | 15 +++++ .../recollect_waste/translations/hu.json | 3 + .../components/rfxtrx/translations/hu.json | 39 ++++++++++- .../components/risco/translations/hu.json | 27 ++++++++ .../translations/hu.json | 3 +- .../components/roomba/translations/hu.json | 7 ++ .../components/roon/translations/hu.json | 3 + .../components/rpi_power/translations/hu.json | 1 + .../components/samsungtv/translations/hu.json | 4 ++ .../screenlogic/translations/hu.json | 1 + .../components/sensor/translations/hu.json | 9 ++- .../components/sentry/translations/hu.json | 16 +++++ .../components/sia/translations/hu.json | 41 +++++++++++- .../components/sma/translations/hu.json | 27 ++++++++ .../components/smappee/translations/hu.json | 5 ++ .../smartthings/translations/hu.json | 1 + .../components/smarttub/translations/hu.json | 4 ++ .../components/solaredge/translations/hu.json | 1 + .../somfy_mylink/translations/hu.json | 11 ++- .../components/sonarr/translations/hu.json | 1 + .../components/sonos/translations/hu.json | 1 + .../components/spotify/translations/hu.json | 4 +- .../srp_energy/translations/hu.json | 1 + .../components/subaru/translations/hu.json | 6 ++ .../components/syncthing/translations/hu.json | 22 ++++++ .../synology_dsm/translations/hu.json | 9 +++ .../system_bridge/translations/hu.json | 31 ++++++++- .../components/tasmota/translations/hu.json | 6 ++ .../tellduslive/translations/hu.json | 5 ++ .../components/toon/translations/hu.json | 1 + .../totalconnect/translations/hu.json | 11 ++- .../components/tplink/translations/hu.json | 5 ++ .../components/tuya/translations/hu.json | 2 + .../components/twinkly/translations/hu.json | 4 ++ .../components/unifi/translations/hu.json | 7 +- .../components/upnp/translations/hu.json | 10 +++ .../components/vacuum/translations/ar.json | 1 + .../components/verisure/translations/hu.json | 3 +- .../components/wallbox/translations/hu.json | 4 +- .../waze_travel_time/translations/hu.json | 10 ++- .../components/wemo/translations/hu.json | 5 ++ .../components/wilight/translations/hu.json | 5 +- .../components/wled/translations/hu.json | 9 +++ .../wolflink/translations/sensor.hu.json | 2 + .../xiaomi_miio/translations/hu.json | 41 +++++++++++- .../yamaha_musiccast/translations/hu.json | 10 ++- .../components/yeelight/translations/hu.json | 3 + .../components/zha/translations/hu.json | 18 +++++ .../zoneminder/translations/hu.json | 8 ++- .../components/zwave_js/translations/hu.json | 67 ++++++++++++++++++- 151 files changed, 1618 insertions(+), 98 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/hu.json create mode 100644 homeassistant/components/coinbase/translations/hu.json create mode 100644 homeassistant/components/ezviz/translations/hu.json create mode 100644 homeassistant/components/freedompro/translations/hu.json create mode 100644 homeassistant/components/ialarm/translations/hu.json create mode 100644 homeassistant/components/kostal_plenticore/translations/hu.json create mode 100644 homeassistant/components/motioneye/translations/hu.json create mode 100644 homeassistant/components/mutesync/translations/hu.json create mode 100644 homeassistant/components/nam/translations/hu.json create mode 100644 homeassistant/components/nmap_tracker/translations/hu.json create mode 100644 homeassistant/components/picnic/translations/hu.json create mode 100644 homeassistant/components/sma/translations/hu.json create mode 100644 homeassistant/components/syncthing/translations/hu.json diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index 260416b07bb..a4ce211d21a 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -20,7 +20,8 @@ "data": { "password": "Jelsz\u00f3", "username": "E-mail" - } + }, + "title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait" }, "user": { "data": { diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index 8a0f7f5a198..ce4721693f3 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -25,5 +25,11 @@ "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00c9rje el az AccuWeather szervert", + "remaining_requests": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek" + } } } \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 251b72574ee..22fb5539bfa 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "hassio_confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant-ot, hogy csatlakozzon az AdGuard Home-hoz, amelyet a kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", + "title": "Az AdGuard Home a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" + }, "user": { "data": { "host": "Hoszt", diff --git a/homeassistant/components/aemet/translations/hu.json b/homeassistant/components/aemet/translations/hu.json index d810691046e..31a7654efd9 100644 --- a/homeassistant/components/aemet/translations/hu.json +++ b/homeassistant/components/aemet/translations/hu.json @@ -14,8 +14,18 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "Az integr\u00e1ci\u00f3 neve" }, + "description": "\u00c1ll\u00edtsa be az AEMET OpenData integr\u00e1ci\u00f3t. Az API-kulcs el\u0151\u00e1ll\u00edt\u00e1s\u00e1hoz keresse fel a https://opendata.aemet.es/centrodedescargas/altaUsuario webhelyet.", "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gy\u0171jts\u00f6n adatokat az AEMET meteorol\u00f3giai \u00e1llom\u00e1sokr\u00f3l" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index b9fd0c9e05c..f730edde85f 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -22,6 +22,7 @@ }, "system_health": { "info": { + "can_reach_server": "\u00c9rje el az Airly szervert", "requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta", "requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek" } diff --git a/homeassistant/components/airnow/translations/hu.json b/homeassistant/components/airnow/translations/hu.json index 418450f2419..3f1bef471ee 100644 --- a/homeassistant/components/airnow/translations/hu.json +++ b/homeassistant/components/airnow/translations/hu.json @@ -14,8 +14,10 @@ "data": { "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" + "longitude": "Hossz\u00fas\u00e1g", + "radius": "\u00c1llom\u00e1s sugara (m\u00e9rf\u00f6ld; opcion\u00e1lis)" }, + "description": "\u00c1ll\u00edtsa be az AirNow leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3t. Az API-kulcs el\u0151\u00e1ll\u00edt\u00e1s\u00e1hoz keresse fel a https://docs.airnowapi.org/account/request/ oldalt.", "title": "AirNow" } } diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 704ce33ab67..e7c47e93793 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "general_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "location_not_found": "A hely nem tal\u00e1lhat\u00f3" }, "step": { "geography_by_coords": { @@ -15,14 +16,19 @@ "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" - } + }, + "description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t a sz\u00e9less\u00e9g / hossz\u00fas\u00e1g figyel\u00e9s\u00e9hez.", + "title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t" }, "geography_by_name": { "data": { "api_key": "API kulcs", "city": "V\u00e1ros", - "country": "Orsz\u00e1g" - } + "country": "Orsz\u00e1g", + "state": "\u00e1llapot" + }, + "description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t egy v\u00e1ros / \u00e1llam / orsz\u00e1g figyel\u00e9s\u00e9hez.", + "title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t" }, "node_pro": { "data": { @@ -33,7 +39,8 @@ "reauth_confirm": { "data": { "api_key": "API kulcs" - } + }, + "title": "Az AirVisual \u00fajb\u00f3li hiteles\u00edt\u00e9se" } } } diff --git a/homeassistant/components/alarm_control_panel/translations/hu.json b/homeassistant/components/alarm_control_panel/translations/hu.json index 81fa10311ef..961006938d9 100644 --- a/homeassistant/components/alarm_control_panel/translations/hu.json +++ b/homeassistant/components/alarm_control_panel/translations/hu.json @@ -4,13 +4,18 @@ "arm_away": "{entity_name} \u00e9les\u00edt\u00e9se t\u00e1voz\u00f3 m\u00f3dban", "arm_home": "{entity_name} \u00e9les\u00edt\u00e9se otthon marad\u00f3 m\u00f3dban", "arm_night": "{entity_name} \u00e9les\u00edt\u00e9se \u00e9jszakai m\u00f3dban", + "arm_vacation": "\u00c9les\u00edtse az {entity_name} a nyaral\u00e1sra", "disarm": "{entity_name} hat\u00e1stalan\u00edt\u00e1sa", "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" }, + "condition_type": { + "is_armed_vacation": "{entity_name} nyaral\u00e1s \u00e9les\u00edtve" + }, "trigger_type": { "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", "armed_home": "{entity_name} otthon marad\u00f3 m\u00f3dban lett \u00e9les\u00edtve", "armed_night": "{entity_name} \u00e9jszakai m\u00f3dban lett \u00e9les\u00edtve", + "armed_vacation": "{entity_name} nyaral\u00e1s \u00e9les\u00edt\u00e9s", "disarmed": "{entity_name} hat\u00e1stalan\u00edtva lett", "triggered": "{entity_name} riaszt\u00e1sba ker\u00fclt" } @@ -22,6 +27,7 @@ "armed_custom_bypass": "\u00c9les\u00edtve \u00e1thidal\u00e1ssal", "armed_home": "\u00c9les\u00edtve otthon", "armed_night": "\u00c9les\u00edtve \u00e9jszaka", + "armed_vacation": "Vak\u00e1ci\u00f3 \u00e9les\u00edt\u00e9s", "arming": "\u00c9les\u00edt\u00e9s", "disarmed": "Hat\u00e1stalan\u00edtva", "disarming": "Hat\u00e1stalan\u00edt\u00e1s", diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json index 8c80adcb3c0..47db325f06c 100644 --- a/homeassistant/components/alarmdecoder/translations/hu.json +++ b/homeassistant/components/alarmdecoder/translations/hu.json @@ -12,28 +12,60 @@ "step": { "protocol": { "data": { + "device_baudrate": "Eszk\u00f6z \u00e1tviteli sebess\u00e9ge", + "device_path": "Eszk\u00f6z el\u00e9r\u00e9si \u00fatja", "host": "Hoszt", "port": "Port" - } + }, + "title": "Konfigur\u00e1lja a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sokat" }, "user": { "data": { "protocol": "Protokoll" - } + }, + "title": "V\u00e1lassza ki a AlarmDecoder protokollt" } } }, "options": { + "error": { + "int": "Az al\u00e1bbi mez\u0151nek eg\u00e9sz sz\u00e1mnak kell lennie.", + "loop_range": "Az RF hurok eg\u00e9sz sz\u00e1m\u00e1nak 1 \u00e9s 4 k\u00f6z\u00f6tt kell lennie.", + "relay_inclusive": "A rel\u00e9c\u00edm \u00e9s a rel\u00e9csatorna egym\u00e1st\u00f3l f\u00fcgg, \u00e9s egy\u00fctt kell felt\u00fcntetni." + }, "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternat\u00edv \u00e9jszakai m\u00f3d", + "auto_bypass": "Automatikus egy\u00e9ni \u00e9les\u00edt\u00e9s", + "code_arm_required": "Az \u00e9les\u00edt\u00e9shez sz\u00fcks\u00e9ges k\u00f3d" + }, + "title": "Konfigur\u00e1lja az AlarmDecodert" + }, "init": { "data": { "edit_select": "Szerkeszt\u00e9s" - } + }, + "description": "Mit szeretn\u00e9l szerkeszteni?", + "title": "Konfigur\u00e1lja az AlarmDecodert" }, "zone_details": { "data": { - "zone_name": "Z\u00f3na neve" - } + "zone_loop": "RF hurok", + "zone_name": "Z\u00f3na neve", + "zone_relayaddr": "Rel\u00e9 c\u00edm", + "zone_relaychan": "Rel\u00e9 csatorna", + "zone_type": "Z\u00f3na t\u00edpusa" + }, + "description": "Adja meg a {zone_number} z\u00f3na adatait. {zone_number} z\u00f3na t\u00f6rl\u00e9s\u00e9hez hagyja \u00fcresen a Z\u00f3na neve elemet.", + "title": "Konfigur\u00e1lja az AlarmDecodert" + }, + "zone_select": { + "data": { + "zone_number": "Z\u00f3na sz\u00e1ma" + }, + "description": "\u00cdrja be a hozz\u00e1adni, szerkeszteni vagy elt\u00e1vol\u00edtani k\u00edv\u00e1nt z\u00f3nasz\u00e1mot.", + "title": "Konfigur\u00e1lja az AlarmDecodert" } } } diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 556412764a2..4cf99c596f0 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -10,7 +10,8 @@ "step": { "reauth_confirm": { "data": { - "api_key": "API kulcs" + "api_key": "API kulcs", + "description": "Hiteles\u00edtse mag\u00e1t \u00fajra az Ambee-fi\u00f3kj\u00e1val." } }, "user": { @@ -19,7 +20,8 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" - } + }, + "description": "\u00c1ll\u00edtsa be az Ambee-t a Homeassistanttal val\u00f3 integr\u00e1ci\u00f3hoz." } } } diff --git a/homeassistant/components/ambee/translations/sensor.hu.json b/homeassistant/components/ambee/translations/sensor.hu.json index aa86baf2722..975d200a507 100644 --- a/homeassistant/components/ambee/translations/sensor.hu.json +++ b/homeassistant/components/ambee/translations/sensor.hu.json @@ -3,6 +3,7 @@ "ambee__risk": { "high": "Magas", "low": "Alacsony", + "moderate": "M\u00e9rs\u00e9kelt", "very high": "Nagyon magas" } } diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 72b334849a0..2b6275fc9f5 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", + "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", + "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -10,35 +13,52 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "no_usable_service": "Tal\u00e1ltunk egy eszk\u00f6zt, de nem tudtuk azonos\u00edtani, hogyan lehetne kapcsolatot l\u00e9tes\u00edteni vele. Ha tov\u00e1bbra is ezt az \u00fczenetet l\u00e1tja, pr\u00f3b\u00e1lja meg megadni az IP-c\u00edm\u00e9t, vagy ind\u00edtsa \u00fajra az Apple TV-t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name}", "step": { "confirm": { + "description": "Arra k\u00e9sz\u00fcl, hogy felvegye a (z) {name} nev\u0171 Apple TV-t a Home Assistant programba. \n\n ** A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN-k\u00f3dot kell megadnia. ** \n\n Felh\u00edvjuk figyelm\u00e9t, hogy ezzel az integr\u00e1ci\u00f3val * nem fogja tudni kikapcsolni az Apple TV-t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja kapcsol ki!", "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be az Apple TV {pin} -t.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { "data": { "pin": "PIN-k\u00f3d" }, + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, azaz \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, "reconfigure": { + "description": "Ez az Apple TV csatlakoz\u00e1si neh\u00e9zs\u00e9gekkel k\u00fczd, ez\u00e9rt \u00fajra kell konfigur\u00e1lni.", "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" }, "service_problem": { + "description": "Hiba t\u00f6rt\u00e9nt a(z) \" {protocol} \" protokoll p\u00e1ros\u00edt\u00e1sakor. Ez figyelmen k\u00edv\u00fcl lett hagyva.", "title": "Nem siker\u00fclt hozz\u00e1adni a szolg\u00e1ltat\u00e1st" }, "user": { "data": { "device_input": "Eszk\u00f6z" }, + "description": "El\u0151sz\u00f6r \u00edrja be a hozz\u00e1adni k\u00edv\u00e1nt Apple TV eszk\u00f6znev\u00e9t (pl. Konyha vagy H\u00e1l\u00f3szoba) vagy IP-c\u00edm\u00e9t. Ha valamilyen eszk\u00f6zt automatikusan tal\u00e1ltak a h\u00e1l\u00f3zat\u00e1n, az al\u00e1bb l\u00e1that\u00f3. \n\n Ha nem l\u00e1tja eszk\u00f6z\u00e9t, vagy b\u00e1rmilyen probl\u00e9m\u00e1t tapasztal, pr\u00f3b\u00e1lja meg megadni az eszk\u00f6z IP-c\u00edm\u00e9t. \n\n {devices}", "title": "\u00daj Apple TV be\u00e1ll\u00edt\u00e1sa" } } }, + "options": { + "step": { + "init": { + "data": { + "start_off": "A Home Assistant ind\u00edt\u00e1sakor ne kapcsolja be az eszk\u00f6zt" + }, + "description": "Konfigur\u00e1lja az eszk\u00f6z \u00e1ltal\u00e1nos be\u00e1ll\u00edt\u00e1sait" + } + } + }, "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 4f47781a15c..891150c1038 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "pwd_and_ssh": "Csak jelsz\u00f3 vagy SSH kulcsf\u00e1jlt adjon meg", + "pwd_or_ssh": "K\u00e9rj\u00fck, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -17,8 +19,11 @@ "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", + "protocol": "Haszn\u00e1lhat\u00f3 kommunik\u00e1ci\u00f3s protokoll", + "ssh_key": "Az SSH kulcsf\u00e1jl el\u00e9r\u00e9si \u00fatja (jelsz\u00f3 helyett)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "\u00c1ll\u00edtsa be a sz\u00fcks\u00e9ges param\u00e9tert az \u00fatv\u00e1laszt\u00f3hoz val\u00f3 csatlakoz\u00e1shoz", "title": "AsusWRT" } } @@ -26,6 +31,13 @@ "options": { "step": { "init": { + "data": { + "consider_home": "V\u00e1rakoz\u00e1si m\u00e1sodpercek, miel\u0151tt egy eszk\u00f6zt lecsatlakoztat", + "dnsmasq": "A dnsmasq.leasing f\u00e1jlok helye az \u00fatv\u00e1laszt\u00f3n", + "interface": "Az a fel\u00fclet, amelyr\u0151l statisztik\u00e1kat szeretne kapni (pl. eth0, eth1 stb.)", + "require_ip": "Az eszk\u00f6z\u00f6knek IP-vel kell rendelkezni\u00fck (hozz\u00e1f\u00e9r\u00e9si pont m\u00f3dhoz)", + "track_unknown": "Az ismeretlen / n\u00e9v n\u00e9lk\u00fcli eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se" + }, "title": "AsusWRT Be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index f95d180b4b5..fec6ad93b26 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -23,6 +23,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "Ha a bejelentkez\u00e9si m\u00f3dszer \u201ee-mail\u201d, akkor a felhaszn\u00e1l\u00f3n\u00e9v az e-mail c\u00edm. Ha a bejelentkez\u00e9si m\u00f3dszer \u201etelefon\u201d, akkor a felhaszn\u00e1l\u00f3n\u00e9v a \u201e+ NNNNNNNNNN\u201d form\u00e1tum\u00fa telefonsz\u00e1m.", "title": "August fi\u00f3k be\u00e1ll\u00edt\u00e1sa" }, "validation": { diff --git a/homeassistant/components/bmw_connected_drive/translations/hu.json b/homeassistant/components/bmw_connected_drive/translations/hu.json index 8724f525626..fa3fcf2df57 100644 --- a/homeassistant/components/bmw_connected_drive/translations/hu.json +++ b/homeassistant/components/bmw_connected_drive/translations/hu.json @@ -16,5 +16,15 @@ } } } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Csak olvashat\u00f3 (csak \u00e9rz\u00e9kel\u0151k \u00e9s \u00e9rtes\u00edt\u00e9sek, szolg\u00e1ltat\u00e1sok v\u00e9grehajt\u00e1sa, z\u00e1rol\u00e1s n\u00e9lk\u00fcl)", + "use_location": "Haszn\u00e1lja a Home Assistant hely\u00e9t az aut\u00f3 helymeghat\u00e1roz\u00e1si lek\u00e9rdez\u00e9seihez (a 2014.07.07. el\u0151tt gy\u00e1rtott nem i3/i8 j\u00e1rm\u0171vekhez sz\u00fcks\u00e9ges)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index 9cd2a0be6c1..8b4ebc6be32 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -7,17 +7,30 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rj\u00fck, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", + "session_error": "Munkamenet hiba: Az API nem OK eredm\u00e9nyt ad vissza.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "Bosch SHC: {name}", "step": { + "confirm_discovery": { + "description": "K\u00e9rj\u00fck, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\n K\u00e9szen \u00e1ll a (z) {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra a Home Assistant seg\u00edts\u00e9g\u00e9vel?" + }, + "credentials": { + "data": { + "password": "A Smart Home Controller jelszava" + } + }, "reauth_confirm": { + "description": "A bosch_shc integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { "data": { "host": "Hoszt" - } + }, + "description": "\u00c1ll\u00edtsa be a Bosch intelligens otthoni vez\u00e9rl\u0151t, hogy lehet\u0151v\u00e9 tegye a fel\u00fcgyeletet \u00e9s a vez\u00e9rl\u00e9st a Home Assistant seg\u00edts\u00e9g\u00e9vel.", + "title": "SHC hiteles\u00edt\u00e9si param\u00e9terek" } } }, diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index 90213e99aec..8b8dce984e5 100644 --- a/homeassistant/components/broadlink/translations/hu.json +++ b/homeassistant/components/broadlink/translations/hu.json @@ -5,6 +5,7 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "not_supported": "Az eszk\u00f6z nem t\u00e1mogatott", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/buienradar/translations/hu.json b/homeassistant/components/buienradar/translations/hu.json new file mode 100644 index 00000000000..a064fa943a8 --- /dev/null +++ b/homeassistant/components/buienradar/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "A kamera k\u00e9peinek megjelen\u00edt\u00e9s\u00e9hez az orsz\u00e1g k\u00f3dja.", + "delta": "A kamera k\u00e9pfriss\u00edt\u00e9s\u00e9nek id\u0151tartama m\u00e1sodpercekben", + "timeframe": "Percek, hogy el\u0151retekints\u00fcnk a csapad\u00e9k el\u0151rejelz\u00e9s\u00e9re" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/hu.json b/homeassistant/components/canary/translations/hu.json index 85dd503a175..77c72901742 100644 --- a/homeassistant/components/canary/translations/hu.json +++ b/homeassistant/components/canary/translations/hu.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "A kamer\u00e1khoz az ffmpeg-nek \u00e1tadott argumentumok", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (opcion\u00e1lis)" } } diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 3b5840b1c14..0f64f8de6fe 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -22,6 +22,23 @@ "options": { "error": { "invalid_known_hosts": "Az ismert hosztoknak vessz\u0151vel elv\u00e1lasztott hosztok list\u00e1j\u00e1nak kell lennie." + }, + "step": { + "advanced_options": { + "data": { + "ignore_cec": "A CEC figyelmen k\u00edv\u00fcl hagy\u00e1sa", + "uuid": "Enged\u00e9lyezett UUID-k" + }, + "description": "Enged\u00e9lyezett UUID - vessz\u0151vel elv\u00e1lasztott lista a Cast-eszk\u00f6z\u00f6k UUID-j\u00e9b\u0151l, amelyeket hozz\u00e1 lehet adni a Home Assistanthoz. Csak akkor haszn\u00e1lja, ha nem akarja hozz\u00e1adni az \u00f6sszes rendelkez\u00e9sre \u00e1ll\u00f3 cast eszk\u00f6zt.\n CEC figyelmen k\u00edv\u00fcl hagy\u00e1sa - vessz\u0151vel elv\u00e1lasztott Chromecast-lista, amelynek figyelmen k\u00edv\u00fcl kell hagynia a CEC-adatokat az akt\u00edv bemenet meghat\u00e1roz\u00e1s\u00e1hoz. Ezt tov\u00e1bb\u00edtjuk a pychromecast.IGNORE_CEC c\u00edmre.", + "title": "Speci\u00e1lis Google Cast-konfigur\u00e1ci\u00f3" + }, + "basic_options": { + "data": { + "known_hosts": "Ismert gazd\u00e1k" + }, + "description": "Ismert gazd\u00e1k - vessz\u0151vel elv\u00e1lasztott lista az eszk\u00f6z\u00f6k hosztneveir\u0151l vagy IP-c\u00edmeir\u0151l, akkor haszn\u00e1lja, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik.", + "title": "Google Cast konfigur\u00e1ci\u00f3" + } } } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index 6d97a51b530..909a5cdf1b5 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "rate_limited": "Jelenleg korl\u00e1tozott sebess\u00e9g\u0171, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -21,6 +22,10 @@ "options": { "step": { "init": { + "data": { + "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" + }, + "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", "title": "Friss\u00edtse a ClimaCell be\u00e1ll\u00edt\u00e1sokat" } } diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json index a0f250376da..ef2a47e2e0d 100644 --- a/homeassistant/components/cloudflare/translations/hu.json +++ b/homeassistant/components/cloudflare/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API token", + "description": "Hiteles\u00edtse \u00fajra Cloudflare-fi\u00f3kj\u00e1val." + } + }, "records": { "data": { "records": "Rekordok" @@ -21,6 +28,7 @@ "data": { "api_token": "API Token" }, + "description": "Ehhez az integr\u00e1ci\u00f3hoz a Z\u00f3na: Z\u00f3na: Olvas\u00e1s \u00e9s Z\u00f3na: DNS: L\u00e9trehozott API-token sz\u00fcks\u00e9ges. A fi\u00f3k \u00f6sszes z\u00f3n\u00e1j\u00e1nak enged\u00e9lyeinek szerkeszt\u00e9se.", "title": "Csatlakoz\u00e1s a Cloudflare szolg\u00e1ltat\u00e1shoz" }, "zone": { diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json new file mode 100644 index 00000000000..5fb22f9be3b --- /dev/null +++ b/homeassistant/components/coinbase/translations/hu.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "Ismeretlen hiba" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "api_token": "API jelsz\u00f3", + "currencies": "Sz\u00e1mlaegyenleg-p\u00e9nznemek", + "exchange_rates": "\u00c1rfolyamok" + }, + "description": "K\u00e9rj\u00fck, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", + "title": "Coinbase API kulcs r\u00e9szletei" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "A k\u00e9rt valutaegyenlegek k\u00f6z\u00fcl egyet vagy t\u00f6bbet nem biztos\u00edt a Coinbase API.", + "exchange_rate_unavaliable": "A k\u00e9rt \u00e1rfolyamok k\u00f6z\u00fcl egyet vagy t\u00f6bbet a Coinbase nem biztos\u00edt.", + "unknown": "Ismeretlen hiba" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Jelentend\u0151 p\u00e9nzt\u00e1rca egyenlegek.", + "exchange_base": "Az \u00e1rfolyam-\u00e9rz\u00e9kel\u0151k alapvalut\u00e1ja.", + "exchange_rate_currencies": "Jelentend\u0151 \u00e1rfolyamok." + }, + "description": "\u00c1ll\u00edtsa be a Coinbase opci\u00f3kat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index cf688d6fdeb..bf67763ca6b 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "Hoszt" + "host": "Hoszt", + "off": "Ki lehet kapcsolni" } } } diff --git a/homeassistant/components/coronavirus/translations/hu.json b/homeassistant/components/coronavirus/translations/hu.json index 631454ec045..9b79c82a014 100644 --- a/homeassistant/components/coronavirus/translations/hu.json +++ b/homeassistant/components/coronavirus/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Nem siker\u00fclt csatlakozni" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 0463463c0b3..84493ccb9f6 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -14,6 +14,7 @@ "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})", "step": { "hassio_confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant-ot, hogy csatlakozzon a (z) {addon} \u00e1ltal biztos\u00edtott deCONZ \u00e1tj\u00e1r\u00f3hoz?", "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Supervisor kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "link": { @@ -89,7 +90,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", - "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se" + "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se", + "allow_new_devices": "Enged\u00e9lyezze az \u00faj eszk\u00f6z\u00f6k automatikus hozz\u00e1ad\u00e1s\u00e1t" }, "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" } diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 2ae4bc69b55..e6727d3c29f 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "update_audyssey": "Friss\u00edtse az Audyssey be\u00e1ll\u00edt\u00e1sait" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json index 4fa10a2a088..391eeb60727 100644 --- a/homeassistant/components/devolo_home_control/translations/hu.json +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "reauth_failed": "K\u00e9rj\u00fck, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." }, "step": { "user": { @@ -13,6 +15,13 @@ "password": "Jelsz\u00f3", "username": "E-mail / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Jelsz\u00f3", + "username": "E-mail / devolo azonos\u00edt\u00f3" + } } } } diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json index 76ad4dc653f..86a15e99aab 100644 --- a/homeassistant/components/dsmr/translations/hu.json +++ b/homeassistant/components/dsmr/translations/hu.json @@ -2,24 +2,45 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "cannot_communicate": "Nem siker\u00fclt csatlakozni." + "cannot_communicate": "Nem siker\u00fclt csatlakozni.", + "cannot_connect": "Nem siker\u00fclt csatlakozni" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_communicate": "Nem siker\u00fclt kommunik\u00e1lni", + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "one": "\u00dcres", + "other": "\u00dcres" }, "step": { + "one": "\u00dcres", + "other": "\u00dcres", "setup_network": { "data": { - "dsmr_version": "DSMR verzi\u00f3 kiv\u00e1laszt\u00e1sa" - } + "dsmr_version": "DSMR verzi\u00f3 kiv\u00e1laszt\u00e1sa", + "host": "Gazdag\u00e9p", + "port": "Port" + }, + "title": "V\u00e1lassza ki a csatlakoz\u00e1si c\u00edmet" }, "setup_serial": { "data": { + "dsmr_version": "DSMR verzi\u00f3 kiv\u00e1laszt\u00e1sa", "port": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" }, "title": "Eszk\u00f6z" }, + "setup_serial_manual_path": { + "data": { + "port": "USB eszk\u00f6z \u00fatvonala" + }, + "title": "\u00datvonal" + }, "user": { "data": { "type": "Kapcsolat t\u00edpusa" - } + }, + "title": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t" } } }, diff --git a/homeassistant/components/eafm/translations/hu.json b/homeassistant/components/eafm/translations/hu.json index 3b2d79a34a7..38863029f12 100644 --- a/homeassistant/components/eafm/translations/hu.json +++ b/homeassistant/components/eafm/translations/hu.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "station": "\u00c1llom\u00e1s" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/econet/translations/hu.json b/homeassistant/components/econet/translations/hu.json index 065c648d4a0..0f9cf18f203 100644 --- a/homeassistant/components/econet/translations/hu.json +++ b/homeassistant/components/econet/translations/hu.json @@ -14,7 +14,8 @@ "data": { "email": "E-mail", "password": "Jelsz\u00f3" - } + }, + "title": "\u00c1ll\u00edtsa be a Rheem EcoNet fi\u00f3kot" } } } diff --git a/homeassistant/components/emulated_roku/translations/hu.json b/homeassistant/components/emulated_roku/translations/hu.json index bccfe3bdcab..e733e9801df 100644 --- a/homeassistant/components/emulated_roku/translations/hu.json +++ b/homeassistant/components/emulated_roku/translations/hu.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "advertise_ip": "IP c\u00edm k\u00f6zl\u00e9se", + "advertise_port": "Port k\u00f6zl\u00e9se", "host_ip": "Hoszt IP c\u00edm", "listen_port": "Port figyel\u00e9se", - "name": "N\u00e9v" + "name": "N\u00e9v", + "upnp_bind_multicast": "K\u00f6t\u00f6tt multicast (igaz/hamis)" }, "title": "A kiszolg\u00e1l\u00f3 szerver konfigur\u00e1l\u00e1sa" } diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json index 3449489bd87..ab92a4ad2bb 100644 --- a/homeassistant/components/enphase_envoy/translations/hu.json +++ b/homeassistant/components/enphase_envoy/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json index 4f70feb6ec1..8e0d7ec9a18 100644 --- a/homeassistant/components/epson/translations/hu.json +++ b/homeassistant/components/epson/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "powered_off": "A projektor be van kapcsolva? A kezdeti konfigur\u00e1l\u00e1shoz be kell kapcsolnia a kivet\u00edt\u0151t." }, "step": { "user": { diff --git a/homeassistant/components/ezviz/translations/hu.json b/homeassistant/components/ezviz/translations/hu.json new file mode 100644 index 00000000000..3ece0a79dcf --- /dev/null +++ b/homeassistant/components/ezviz/translations/hu.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_host": "\u00c9rv\u00e9nytelen gazdag\u00e9pn\u00e9v vagy IP-c\u00edm" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "\u00cdrja be az RTSP-hiteles\u00edt\u0151 adatokat az Ezviz {serial} kamer\u00e1hoz IP- {ip_address}", + "title": "Felfedezett Ezviz kamera" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "url": "URL", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Csatlakozzon az Ezviz Cloud szolg\u00e1ltat\u00e1shoz" + }, + "user_custom_url": { + "data": { + "password": "Jelsz\u00f3", + "url": "URL", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg k\u00e9zzel a r\u00e9gi\u00f3 URL-j\u00e9t", + "title": "Csatlakozzon az Ezviz-hez egy\u00e9ni URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "A kamer\u00e1khoz az ffmpeg-nek \u00e1tadott argumentumok", + "timeout": "K\u00e9r\u00e9s id\u0151korl\u00e1tja (m\u00e1sodperc)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json index 8e8432d5df4..54461091c93 100644 --- a/homeassistant/components/fireservicerota/translations/hu.json +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -14,7 +14,8 @@ "reauth": { "data": { "password": "Jelsz\u00f3" - } + }, + "description": "A hiteles\u00edt\u00e9si tokenek \u00e9rv\u00e9nytelenn\u00e9 v\u00e1ltak, a l\u00e9trehoz\u00e1shoz jelentkezzen be." }, "user": { "data": { diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json index cc0c820facf..e607ac4255e 100644 --- a/homeassistant/components/flume/translations/hu.json +++ b/homeassistant/components/flume/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A(z) {username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", + "title": "Hiteles\u00edtse \u00fajra Flume-fi\u00f3kj\u00e1t" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index b863d10e907..0bd814f16be 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -3,8 +3,28 @@ "step": { "user": { "data": { + "azimuth": "Azimut (360 fok, 0 = \u00e9szak, 90 = keleti, 180 = d\u00e9li, 270 = nyugati)", + "declination": "Deklin\u00e1ci\u00f3 (0 = v\u00edzszintes, 90 = f\u00fcgg\u0151leges)", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)", "name": "N\u00e9v" - } + }, + "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API kulcs (opcion\u00e1lis)", + "azimuth": "Azimut (360 fok, 0 = \u00e9szak, 90 = keleti, 180 = d\u00e9li, 270 = nyugati)", + "damping": "Csillap\u00edt\u00e1si t\u00e9nyez\u0151: be\u00e1ll\u00edtja az eredm\u00e9nyeket reggelre \u00e9s est\u00e9re", + "declination": "Deklin\u00e1ci\u00f3 (0 = v\u00edzszintes, 90 = f\u00fcgg\u0151leges)", + "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" + }, + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." } } } diff --git a/homeassistant/components/freedompro/translations/hu.json b/homeassistant/components/freedompro/translations/hu.json new file mode 100644 index 00000000000..e56cc7a4a41 --- /dev/null +++ b/homeassistant/components/freedompro/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs" + }, + "description": "K\u00e9rj\u00fck, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", + "title": "Freedompro API kulcs" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index eda37325071..1433860bfa6 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -1,16 +1,62 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" + }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "connection_error": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "flow_title": "{name}", "step": { + "confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Felfedezte a FRITZ! Boxot: {name} \n\n A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa a {name}", + "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa" + }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "{host} FRITZ! Box Tools hiteles\u00edt\u0151 adatait. \n\n A FRITZ! Box Tools nem tud bejelentkezni a FRITZ! Box eszk\u00f6zbe.", + "title": "A FRITZ! Box Tools friss\u00edt\u00e9se - hiteles\u00edt\u0151 adatok" + }, + "start_config": { + "data": { + "host": "Gazdag\u00e9p", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "A FRITZ! Box eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a FRITZ! Box vez\u00e9rl\u00e9s\u00e9hez.\n Minimum sz\u00fcks\u00e9ges: felhaszn\u00e1l\u00f3n\u00e9v, jelsz\u00f3.", + "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa - k\u00f6telez\u0151" + }, "user": { "data": { "host": "Hoszt", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "A FRITZ! Box eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a FRITZ! Box vez\u00e9rl\u00e9s\u00e9hez.\n Minimum sz\u00fcks\u00e9ges: felhaszn\u00e1l\u00f3n\u00e9v, jelsz\u00f3.", + "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "M\u00e1sodpercek egy eszk\u00f6z \"otthon\" tart\u00e1s\u00e1ra" } } } diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 630b15b990c..81639b1d830 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -22,7 +22,8 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "Friss\u00edtse a(z) {name} bejelentkez\u00e9si adatait." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/hu.json b/homeassistant/components/fritzbox_callmonitor/translations/hu.json index 3255d205fa1..5006dd77f14 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/hu.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "insufficient_permissions": "A felhaszn\u00e1l\u00f3nak nincs elegend\u0151 enged\u00e9lye az AVM FRITZ! Box be\u00e1ll\u00edt\u00e1sainak \u00e9s telefonk\u00f6nyveinek el\u00e9r\u00e9s\u00e9hez.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { @@ -23,5 +24,18 @@ } } } + }, + "options": { + "error": { + "malformed_prefixes": "Az el\u0151tagok hib\u00e1san vannak form\u00e1zva, ellen\u0151rizze a form\u00e1tumukat." + }, + "step": { + "init": { + "data": { + "prefixes": "El\u0151tagok (vessz\u0151vel elv\u00e1lasztott lista)" + }, + "title": "Konfigur\u00e1lja az el\u0151tagokat" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/hu.json b/homeassistant/components/garages_amsterdam/translations/hu.json index c02cd4077ba..66bc29c02fa 100644 --- a/homeassistant/components/garages_amsterdam/translations/hu.json +++ b/homeassistant/components/garages_amsterdam/translations/hu.json @@ -4,6 +4,15 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "garage_name": "Gar\u00e1zs neve" + }, + "title": "V\u00e1lasszon egy gar\u00e1zst a megfigyel\u00e9shez" + } } - } + }, + "title": "Gar\u00e1zsok Amszterdam" } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/hu.json b/homeassistant/components/gios/translations/hu.json index b35904e9d76..9454aceb13d 100644 --- a/homeassistant/components/gios/translations/hu.json +++ b/homeassistant/components/gios/translations/hu.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Lengyel K\u00f6rnyezetv\u00e9delmi F\u0151fel\u00fcgyel\u0151s\u00e9g)" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00c9rje el a GIO\u015a szervert" + } } } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index ebc56c1bbe5..f8c507a6625 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -11,11 +11,17 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "confirm_discovery": { + "description": "DHCP foglal\u00e1s aj\u00e1nlott az \u00fatv\u00e1laszt\u00f3n. Ha nincs be\u00e1ll\u00edtva, akkor az eszk\u00f6z el\u00e9rhetetlenn\u00e9 v\u00e1lhat, am\u00edg a Home Assistant \u00e9szleli az \u00faj IP-c\u00edmet. Olvassa el az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Hoszt", "name": "N\u00e9v" - } + }, + "description": "El\u0151sz\u00f6r le kell t\u00f6ltenie a Goal Zero alkalmaz\u00e1st: https://www.goalzero.com/product-features/yeti-app/ \n\nK\u00f6vesse az utas\u00edt\u00e1sokat, hogy csatlakoztassa Yeti k\u00e9sz\u00fcl\u00e9k\u00e9t a Wi-Fi h\u00e1l\u00f3zathoz. DHCP foglal\u00e1s aj\u00e1nlott az \u00fatv\u00e1laszt\u00f3n. Ha nincs be\u00e1ll\u00edtva, akkor az eszk\u00f6z el\u00e9rhetetlenn\u00e9 v\u00e1lhat, am\u00edg a Home Assistant \u00e9szleli az \u00faj IP-c\u00edmet. Olvassa el az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t.", + "title": "Goal Zero Yeti" } } } diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json index 5bee8045c4f..85a15a98e58 100644 --- a/homeassistant/components/google_travel_time/translations/hu.json +++ b/homeassistant/components/google_travel_time/translations/hu.json @@ -11,6 +11,7 @@ "data": { "api_key": "Api kucs", "destination": "C\u00e9l", + "name": "N\u00e9v", "origin": "Eredet" }, "description": "Az eredet \u00e9s a c\u00e9l megad\u00e1sakor megadhat egy vagy t\u00f6bb helyet a pipa karakterrel elv\u00e1lasztva, c\u00edm, sz\u00e9less\u00e9gi / hossz\u00fas\u00e1gi koordin\u00e1t\u00e1k vagy Google helyazonos\u00edt\u00f3 form\u00e1j\u00e1ban. Amikor a helyet megadja egy Google helyazonos\u00edt\u00f3val, akkor az azonos\u00edt\u00f3t el\u0151taggal kell ell\u00e1tni a `hely_azonos\u00edt\u00f3:` sz\u00f3val." diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json index ff2c2fc87b5..d856d13a96b 100644 --- a/homeassistant/components/growatt_server/translations/hu.json +++ b/homeassistant/components/growatt_server/translations/hu.json @@ -1,11 +1,27 @@ { "config": { + "abort": { + "no_plants": "Ezen a sz\u00e1ml\u00e1n nem tal\u00e1ltak n\u00f6v\u00e9nyeket" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { + "plant": { + "data": { + "plant_id": "N\u00f6v\u00e9ny" + }, + "title": "V\u00e1lassza ki a n\u00f6v\u00e9ny\u00e9t" + }, "user": { "data": { - "password": "Jelsz\u00f3" - } + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Adja meg Growatt adatait" } } - } + }, + "title": "Growatt szerver" } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index bd43ce7672c..ca9a746f9d9 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -6,6 +6,9 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "discovery_confirm": { + "description": "Be akarja \u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" + }, "user": { "data": { "ip_address": "IP c\u00edm", diff --git a/homeassistant/components/habitica/translations/hu.json b/homeassistant/components/habitica/translations/hu.json index 4914a1bd27a..589f53e852c 100644 --- a/homeassistant/components/habitica/translations/hu.json +++ b/homeassistant/components/habitica/translations/hu.json @@ -9,8 +9,10 @@ "data": { "api_key": "API kulcs", "api_user": "Habitica API felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja", + "name": "A Habitica felhaszn\u00e1l\u00f3n\u00e9v fel\u00fcl\u00edr\u00e1sa. A szolg\u00e1ltat\u00e1si h\u00edv\u00e1sokhoz lesz haszn\u00e1lva", "url": "URL" - } + }, + "description": "Csatlakoztassa Habitica-profilj\u00e1t, hogy figyelemmel k\u00eds\u00e9rhesse felhaszn\u00e1l\u00f3i profilj\u00e1t \u00e9s feladatait. Ne feledje, hogy az api_id \u00e9s api_key c\u00edmeket a https://habitica.com/user/settings/api webhelyr\u0151l kell beszerezni" } } }, diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index b81e3fcf0dd..3c065b01169 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "\u00dcres", "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" }, "user": { @@ -21,6 +22,7 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, + "description": "\u00dcres", "title": "Google Hangouts Bejelentkez\u00e9s" } } diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index ae7a51a8ec8..64b0f26ae46 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "board": "Alaplap", "disk_total": "\u00d6sszes hely", "disk_used": "Felhaszn\u00e1lt hely", "docker_version": "Docker verzi\u00f3", diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index 80c6a7e40f1..ce07abcb338 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -31,6 +31,7 @@ "user": { "data": { "password": "Jelsz\u00f3", + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Add meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.", @@ -41,6 +42,10 @@ "options": { "step": { "user": { + "data": { + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" + }, + "description": "Friss\u00edtse a vizsg\u00e1lati intervallumot az adatok gyakrabban t\u00f6rt\u00e9n\u0151 lek\u00e9rdez\u00e9s\u00e9hez.", "title": "Hive be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 90e2405ed64..cd06d12e809 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -7,10 +7,12 @@ "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", + "invalid_properties": "Az eszk\u00f6z \u00e1ltal bejelentett \u00e9rv\u00e9nytelen tulajdons\u00e1gok.", "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" }, "error": { "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "insecure_setup_code": "A k\u00e9rt telep\u00edt\u00e9si k\u00f3d trivi\u00e1lis jellege miatt nem biztons\u00e1gos. Ez a tartoz\u00e9k nem felel meg az alapvet\u0151 biztons\u00e1gi k\u00f6vetelm\u00e9nyeknek.", "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs ingyenes p\u00e1ros\u00edt\u00e1si t\u00e1rhely.", "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy az eszk\u00f6z jelenleg m\u00e9g nem t\u00e1mogatott.", "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", @@ -18,13 +20,24 @@ }, "flow_title": "HomeKit tartoz\u00e9k: {name}", "step": { + "busy_error": { + "title": "Az eszk\u00f6z m\u00e1r p\u00e1rosul egy m\u00e1sik vez\u00e9rl\u0151vel" + }, + "max_tries_error": { + "description": "Az eszk\u00f6z t\u00f6bb mint 100 sikertelen hiteles\u00edt\u00e9si k\u00eds\u00e9rletet kapott. Ind\u00edtsa \u00fajra az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1s folytat\u00e1s\u00e1t.", + "title": "T\u00fall\u00e9pte a maxim\u00e1lis hiteles\u00edt\u00e9si k\u00eds\u00e9rleteket" + }, "pair": { "data": { + "allow_insecure_setup_codes": "P\u00e1ros\u00edt\u00e1s enged\u00e9lyez\u00e9se a nem biztons\u00e1gos be\u00e1ll\u00edt\u00e1si k\u00f3dokkal.", "pairing_code": "P\u00e1ros\u00edt\u00e1si k\u00f3d" }, "description": "\u00cdrja be a HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban) a kieg\u00e9sz\u00edt\u0151 haszn\u00e1lat\u00e1hoz", "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" }, + "protocol_error": { + "title": "Hiba t\u00f6rt\u00e9nt a tartoz\u00e9kkal val\u00f3 kommunik\u00e1ci\u00f3 sor\u00e1n" + }, "user": { "data": { "device": "Eszk\u00f6z" diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index eaa8d8834c3..90fee286a3a 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -21,6 +21,7 @@ "title": "V\u00e1lassz HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" }, "link": { + "description": "A HomematicIP regisztr\u00e1l\u00e1s\u00e1hoz a Home Assistant alkalmaz\u00e1sban nyomja meg a hozz\u00e1f\u00e9r\u00e9si pont k\u00e9k gombj\u00e1t \u00e9s a bek\u00fcld\u00e9s gombot. \n\n ! [A gomb helye a h\u00eddon] (/ static / images / config_flows / config_homematicip_cloud.png)", "title": "Link Hozz\u00e1f\u00e9r\u00e9si pont" } } diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 5c045d0c6f3..eff9c8a813b 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -31,7 +31,9 @@ "data": { "name": "\u00c9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s neve (a m\u00f3dos\u00edt\u00e1s \u00fajraind\u00edt\u00e1st ig\u00e9nyel)", "recipient": "SMS-\u00e9rtes\u00edt\u00e9s c\u00edmzettjei", - "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomk\u00f6vet\u00e9se" + "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomk\u00f6vet\u00e9se", + "track_wired_clients": "Vezet\u00e9kes h\u00e1l\u00f3zati \u00fcgyfelek nyomon k\u00f6vet\u00e9se", + "unauthenticated_mode": "Nem hiteles\u00edtett m\u00f3d (a v\u00e1ltoztat\u00e1shoz \u00fajrat\u00f6lt\u00e9sre van sz\u00fcks\u00e9g)" } } } diff --git a/homeassistant/components/hvv_departures/translations/hu.json b/homeassistant/components/hvv_departures/translations/hu.json index 91da2d13a7c..deab9bcb929 100644 --- a/homeassistant/components/hvv_departures/translations/hu.json +++ b/homeassistant/components/hvv_departures/translations/hu.json @@ -20,6 +20,9 @@ "options": { "step": { "init": { + "data": { + "offset": "Eltol\u00e1s (perc)" + }, "title": "Be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index 5096423c143..852c108c0e9 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "auth_new_token_not_granted_error": "Az \u00fajonnan l\u00e9trehozott tokent nem hagyt\u00e1k j\u00f3v\u00e1 a Hyperion felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n", + "auth_new_token_not_work_error": "Nem siker\u00fclt hiteles\u00edteni az \u00fajonnan l\u00e9trehozott token haszn\u00e1lat\u00e1val", + "auth_required_error": "Nem siker\u00fclt meghat\u00e1rozni, hogy sz\u00fcks\u00e9ges-e enged\u00e9ly", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_id": "A Hyperion Ambilight p\u00e9ld\u00e1ny nem szolg\u00e1ltatja az azonos\u00edt\u00f3j\u00e1t", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -13,8 +17,21 @@ "step": { "auth": { "data": { - "create_token": "\u00daj token automatikus l\u00e9trehoz\u00e1sa" - } + "create_token": "\u00daj token automatikus l\u00e9trehoz\u00e1sa", + "token": "Vagy adjon meg m\u00e1r l\u00e9tez\u0151 tokent" + }, + "description": "Konfigur\u00e1lja a jogosults\u00e1got a Hyperion Ambilight kiszolg\u00e1l\u00f3hoz" + }, + "confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni ezt a Hyperion Ambilight-ot az Otthoni asszisztenshez? \n\n ** Host: ** {host}\n ** Port: ** {port}\n ** azonos\u00edt\u00f3 **: {id}", + "title": "Er\u0151s\u00edtse meg a Hyperion Ambilight szolg\u00e1ltat\u00e1s hozz\u00e1ad\u00e1s\u00e1t" + }, + "create_token": { + "description": "Az al\u00e1bbiakban v\u00e1lassza a ** K\u00fcld\u00e9s ** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \" {auth_id} \"", + "title": "\u00daj hiteles\u00edt\u00e9si token automatikus l\u00e9trehoz\u00e1sa" + }, + "create_token_external": { + "title": "\u00daj token elfogad\u00e1sa a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcleten" }, "user": { "data": { @@ -28,7 +45,8 @@ "step": { "init": { "data": { - "effect_show_list": "Megjelen\u00edtend\u0151 Hyperion effektusok" + "effect_show_list": "Megjelen\u00edtend\u0151 Hyperion effektusok", + "priority": "Hyperion priorit\u00e1s a sz\u00ednekhez \u00e9s effektekhez" } } } diff --git a/homeassistant/components/ialarm/translations/hu.json b/homeassistant/components/ialarm/translations/hu.json new file mode 100644 index 00000000000..e69c6e7e7ea --- /dev/null +++ b/homeassistant/components/ialarm/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "Gazdag\u00e9p", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json index 06033fb1321..462fae3e1cb 100644 --- a/homeassistant/components/insteon/translations/hu.json +++ b/homeassistant/components/insteon/translations/hu.json @@ -13,7 +13,9 @@ "data": { "host": "IP c\u00edm", "port": "Port" - } + }, + "description": "Konfigur\u00e1lja az Insteon Hub 1. verzi\u00f3j\u00e1t (2014 el\u0151tti).", + "title": "Insteon Hub 1. verzi\u00f3" }, "hubv2": { "data": { @@ -22,12 +24,14 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "Konfigur\u00e1lja az Insteon Hub 2. verzi\u00f3j\u00e1t.", "title": "Insteon Hub 2. verzi\u00f3" }, "plm": { "data": { "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - } + }, + "title": "Insteon PLM" }, "user": { "data": { @@ -47,6 +51,9 @@ "title": "Insteon" }, "add_x10": { + "data": { + "unitcode": "Egys\u00e9gk\u00f3d (1 - 16)" + }, "title": "Insteon" }, "change_hub_config": { @@ -59,12 +66,19 @@ "title": "Insteon" }, "init": { + "data": { + "add_x10": "Adjon hozz\u00e1 egy X10 eszk\u00f6zt." + }, "title": "Insteon" }, "remove_override": { "title": "Insteon" }, "remove_x10": { + "data": { + "address": "V\u00e1lassza ki az elt\u00e1vol\u00edtani k\u00edv\u00e1nt eszk\u00f6z c\u00edm\u00e9t" + }, + "description": "T\u00e1vol\u00edtson el egy X10 eszk\u00f6zt", "title": "Insteon" } } diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index ca8646ec584..065be706d0f 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -25,5 +25,13 @@ "title": "ISY994 Be\u00e1ll\u00edt\u00e1sok" } } + }, + "system_health": { + "info": { + "device_connected": "ISY csatlakozik", + "host_reachable": "El\u00e9rhet\u0151 gazdag\u00e9p", + "last_heartbeat": "Utols\u00f3 sz\u00edvver\u00e9s ideje", + "websocket_status": "Esem\u00e9nySocket \u00e1llapota" + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json index c1d27e9ae07..c2327130a11 100644 --- a/homeassistant/components/keenetic_ndms2/translations/hu.json +++ b/homeassistant/components/keenetic_ndms2/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "no_udn": "Az SSDP felder\u00edt\u00e9si inform\u00e1ci\u00f3knak nincs UDN-je", + "not_keenetic_ndms2": "A felfedezett elem nem Keenetic router" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" @@ -14,6 +16,21 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "\u00c1ll\u00edtsa be a Keenetic NDMS2 t\u00edpus\u00fa routert" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Otthoni intervallumk\u00e9nt vegye figyelembe", + "include_arp": "ARP-adatok haszn\u00e1lata (figyelmen k\u00edv\u00fcl hagyva, ha hotspot-adatokat haszn\u00e1lnak)", + "include_associated": "WiFi AP t\u00e1rs\u00edt\u00e1si adatok haszn\u00e1lata (figyelmen k\u00edv\u00fcl hagyva, ha hotspot adatokat haszn\u00e1lnak)", + "interfaces": "A beolvasni k\u00edv\u00e1nt interf\u00e9szek kiv\u00e1laszt\u00e1sa", + "scan_interval": "Szkennel\u00e9si intervallum", + "try_hotspot": "Haszn\u00e1lja az \u201eip hotspot\u201d adatokat (a legpontosabb)" } } } diff --git a/homeassistant/components/kmtronic/translations/hu.json b/homeassistant/components/kmtronic/translations/hu.json index 0abcc301f0c..4fe9a3875e6 100644 --- a/homeassistant/components/kmtronic/translations/hu.json +++ b/homeassistant/components/kmtronic/translations/hu.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Ford\u00edtott kapcsol\u00f3 logika (NC haszn\u00e1lata)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json index 48ea9d954bd..9ae1e0741d5 100644 --- a/homeassistant/components/kodi/translations/hu.json +++ b/homeassistant/components/kodi/translations/hu.json @@ -4,6 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_uuid": "A Kodi p\u00e9ld\u00e1nynak nincs egyedi azonos\u00edt\u00f3ja. Ennek oka val\u00f3sz\u00edn\u0171leg egy r\u00e9gi Kodi verzi\u00f3 (17.x vagy alacsonyabb). Be\u00e1ll\u00edthatja manu\u00e1lisan az integr\u00e1ci\u00f3t, vagy friss\u00edthet egy \u00fajabb Kodi verzi\u00f3ra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { @@ -29,13 +30,21 @@ "host": "Hoszt", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata" - } + }, + "description": "Kodi csatlakoz\u00e1si inform\u00e1ci\u00f3k. A Rendszer / Be\u00e1ll\u00edt\u00e1sok / H\u00e1l\u00f3zat / Szolg\u00e1ltat\u00e1sok men\u00fcben enged\u00e9lyezze a \"Kodi vez\u00e9rl\u00e9s\u00e9nek enged\u00e9lyez\u00e9se HTTP-n kereszt\u00fcl\" lehet\u0151s\u00e9get." }, "ws_port": { "data": { "ws_port": "Port" - } + }, + "description": "A WebSocket port (n\u00e9ha TCP-portnak h\u00edvj\u00e1k a Kodi-ban). A WebSocketen kereszt\u00fcli kapcsol\u00f3d\u00e1shoz enged\u00e9lyeznie kell a \"Programok enged\u00e9lyez\u00e9se ... a Kodi vez\u00e9rl\u00e9s\u00e9t\" lehet\u0151s\u00e9get a Rendszer / Be\u00e1ll\u00edt\u00e1sok / H\u00e1l\u00f3zat / Szolg\u00e1ltat\u00e1sok men\u00fcben. Ha a WebSocket nincs enged\u00e9lyezve, t\u00e1vol\u00edtsa el a portot, \u00e9s hagyja \u00fcresen." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} kikapcsol\u00e1s\u00e1t k\u00e9rt\u00e9k", + "turn_on": "{entity_name} bekapcsol\u00e1s\u00e1t k\u00e9rt\u00e9k" + } } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/hu.json b/homeassistant/components/kostal_plenticore/translations/hu.json new file mode 100644 index 00000000000..b235578e9c3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "Gazdag\u00e9p", + "password": "Jelsz\u00f3" + } + } + } + }, + "title": "Kostal Plenticore szol\u00e1r inverter" +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/hu.json b/homeassistant/components/kraken/translations/hu.json index 4901da74d90..793a3433eb8 100644 --- a/homeassistant/components/kraken/translations/hu.json +++ b/homeassistant/components/kraken/translations/hu.json @@ -3,10 +3,28 @@ "abort": { "already_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "one": "\u00dcres", + "other": "\u00dcres" + }, "step": { "user": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si intervallum", + "tracked_asset_pairs": "Nyomon k\u00f6vetett eszk\u00f6zp\u00e1rok" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/hu.json b/homeassistant/components/litejet/translations/hu.json index 6d895624c30..3ee53c086bf 100644 --- a/homeassistant/components/litejet/translations/hu.json +++ b/homeassistant/components/litejet/translations/hu.json @@ -3,11 +3,15 @@ "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "open_failed": "A megadott soros port nem nyithat\u00f3 meg." + }, "step": { "user": { "data": { "port": "Port" }, + "description": "Csatlakoztassa a LiteJet RS232-2 portj\u00e1t a sz\u00e1m\u00edt\u00f3g\u00e9p\u00e9hez, \u00e9s adja meg a soros port eszk\u00f6z\u00e9nek el\u00e9r\u00e9si \u00fatj\u00e1t. \n\n A LiteJet MCP-t 19,2 K baudra, 8 adatbitre, 1 stopbitre, parit\u00e1s n\u00e9lk\u00fcl kell konfigur\u00e1lni, \u00e9s minden v\u00e1lasz ut\u00e1n \u201eCR\u201d jelet kell tov\u00e1bb\u00edtania.", "title": "Csatlakoz\u00e1s a LiteJet-hez" } } diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index a8e62b37390..905fc05bf8e 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -2,18 +2,24 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "not_lutron_device": "A felfedezett eszk\u00f6z nem Lutron eszk\u00f6z" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "flow_title": "{name} ({host})", "step": { + "link": { + "description": "A(z) {name} {host} p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", + "title": "P\u00e1ros\u00edtsd a h\u00edddal" + }, "user": { "data": { "host": "Hoszt" }, - "description": "Add meg az eszk\u00f6z IP-c\u00edm\u00e9t." + "description": "Add meg az eszk\u00f6z IP-c\u00edm\u00e9t.", + "title": "Automatikus csatlakoz\u00e1s a h\u00eddhoz" } } }, @@ -23,9 +29,44 @@ "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", "button_4": "Negyedik gomb", + "close_1": "Bez\u00e1r\u00e1s 1.", + "close_2": "Bez\u00e1r\u00e1s 2.", + "close_3": "Bez\u00e1r\u00e1s 3.", + "close_4": "Bez\u00e1r\u00e1s 4.", + "close_all": "Z\u00e1rja be az \u00f6sszeset", + "group_1_button_1": "Els\u0151 csoport els\u0151 gomb", + "group_1_button_2": "Els\u0151 csoport m\u00e1sodik gomb", + "group_2_button_1": "M\u00e1sodik csoport els\u0151 gomb", + "group_2_button_2": "M\u00e1sodik csoport m\u00e1sodik gomb", + "lower": "Als\u00f3", + "lower_1": "Als\u00f3 1", + "lower_2": "Als\u00f3 2", + "lower_3": "Als\u00f3 3", + "lower_4": "Als\u00f3 4", + "lower_all": "Engedje le az \u00f6sszeset", "off": "Ki", "on": "Be", + "open_1": "Nyissa meg az 1-t", + "open_2": "Nyissa meg a 2-at", + "open_3": "Nyissa meg a 3-at", + "open_4": "Nyissa meg a 4-et", + "open_all": "Nyissa meg az \u00f6sszeset", + "raise": "Emel", + "raise_1": "1. emel\u00e9s", + "raise_2": "2. emel\u00e9s", + "raise_3": "3. emel\u00e9s", + "raise_4": "4. emel\u00e9s", + "raise_all": "Emelje fel mindet", + "stop": "Meg\u00e1ll\u00f3 (kedvenc)", + "stop_1": "1. meg\u00e1ll\u00f3", + "stop_2": "2. meg\u00e1ll\u00f3", + "stop_3": "3. meg\u00e1ll\u00f3", + "stop_4": "4. meg\u00e1ll\u00f3", "stop_all": "Az \u00f6sszes le\u00e1ll\u00edt\u00e1sa" + }, + "trigger_type": { + "press": "\"{subtype}\" lenyomva", + "release": "\"{subtype}\" felengedve" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json index cae1f6d20c0..c6174673a90 100644 --- a/homeassistant/components/lyric/translations/hu.json +++ b/homeassistant/components/lyric/translations/hu.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Lyric integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t.", + "title": "Az integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index f881bfbac45..e6b80240184 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -17,6 +17,7 @@ "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3" }, + "description": "K\u00e9rj\u00fck, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt.", "title": "Mazda Connected Services - Fi\u00f3k hozz\u00e1ad\u00e1sa" } } diff --git a/homeassistant/components/media_player/translations/ar.json b/homeassistant/components/media_player/translations/ar.json index 5fa1f1d6df5..c1b45e44893 100644 --- a/homeassistant/components/media_player/translations/ar.json +++ b/homeassistant/components/media_player/translations/ar.json @@ -1,4 +1,12 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u062e\u0627\u0645\u0644" + }, + "trigger_type": { + "idle": "{entity_name} \u0627\u0635\u0628\u062d \u062e\u0627\u0645\u0644\u0627" + } + }, "state": { "_": { "idle": "\u062e\u0627\u0645\u0644", diff --git a/homeassistant/components/met/translations/hu.json b/homeassistant/components/met/translations/hu.json index b9141541a93..38c84a3f8dc 100644 --- a/homeassistant/components/met/translations/hu.json +++ b/homeassistant/components/met/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "A Home Assistant konfigur\u00e1ci\u00f3j\u00e1ban nincsenek megadva otthoni koordin\u00e1t\u00e1k" + }, "error": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, diff --git a/homeassistant/components/met_eireann/translations/hu.json b/homeassistant/components/met_eireann/translations/hu.json index 65108e183a9..b70aa2dcf67 100644 --- a/homeassistant/components/met_eireann/translations/hu.json +++ b/homeassistant/components/met_eireann/translations/hu.json @@ -11,6 +11,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" }, + "description": "Adja meg tart\u00f3zkod\u00e1si hely\u00e9t a Met \u00c9ireann Public Weather Forecast API id\u0151j\u00e1r\u00e1si adatainak haszn\u00e1lat\u00e1hoz", "title": "Elhelyezked\u00e9s" } } diff --git a/homeassistant/components/meteoclimatic/translations/hu.json b/homeassistant/components/meteoclimatic/translations/hu.json index 893a6693c01..582c78f263d 100644 --- a/homeassistant/components/meteoclimatic/translations/hu.json +++ b/homeassistant/components/meteoclimatic/translations/hu.json @@ -6,6 +6,15 @@ }, "error": { "not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "step": { + "user": { + "data": { + "code": "\u00c1llom\u00e1s k\u00f3dja" + }, + "description": "Adja meg a meteorol\u00f3giai \u00e1llom\u00e1s k\u00f3dj\u00e1t (pl. ESCAT4300000043206B)", + "title": "Meteoklimatikus" + } } } } \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/hu.json b/homeassistant/components/metoffice/translations/hu.json index 350e6f92f32..8e4c2dcb8db 100644 --- a/homeassistant/components/metoffice/translations/hu.json +++ b/homeassistant/components/metoffice/translations/hu.json @@ -14,6 +14,7 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" }, + "description": "A f\u00f6ldrajzi sz\u00e9less\u00e9get \u00e9s hossz\u00fas\u00e1got haszn\u00e1ljuk a legk\u00f6zelebbi id\u0151j\u00e1r\u00e1s-\u00e1llom\u00e1s megtal\u00e1l\u00e1s\u00e1hoz.", "title": "Csatlakoz\u00e1s a UK Met Office-hoz" } } diff --git a/homeassistant/components/modern_forms/translations/hu.json b/homeassistant/components/modern_forms/translations/hu.json index b64bf60763a..fee0216224c 100644 --- a/homeassistant/components/modern_forms/translations/hu.json +++ b/homeassistant/components/modern_forms/translations/hu.json @@ -15,8 +15,14 @@ "user": { "data": { "host": "Hoszt" - } + }, + "description": "\u00c1ll\u00edtsa be a Modern Forms-t, hogy integr\u00e1l\u00f3djon a Home Assistant programba." + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni a(z) {name} `nev\u0171 Modern Forms rajong\u00f3t a Home Assistanthoz?", + "title": "Felfedezte a Modern Forms rajong\u00f3i eszk\u00f6zt" } } - } + }, + "title": "Modern Forms" } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 541cefd2110..19f0c70c4d6 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -5,11 +5,15 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "connection_error": "Sikertelen csatlakoz\u00e1s" }, + "error": { + "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t" + }, "step": { "connect": { "data": { "api_key": "API kulcs" - } + }, + "description": "Sz\u00fcks\u00e9ge lesz a 16 karakteres API kulcsra, \u00fatmutat\u00e1s\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" }, "select": { "data": { @@ -20,7 +24,8 @@ "data": { "api_key": "API kulcs", "host": "IP c\u00edm" - } + }, + "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja" } } } diff --git a/homeassistant/components/motioneye/translations/hu.json b/homeassistant/components/motioneye/translations/hu.json new file mode 100644 index 00000000000..5b23c74dc76 --- /dev/null +++ b/homeassistant/components/motioneye/translations/hu.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_url": "\u00c9rv\u00e9nytelen URL", + "unknown": "Ismeretlen hiba" + }, + "step": { + "hassio_confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant, hogy csatlakozzon a(z) {addon} \u00e1ltal biztos\u00edtott motionEye szolg\u00e1ltat\u00e1shoz?", + "title": "motionEye a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" + }, + "user": { + "data": { + "admin_password": "Rendszergazda Jelsz\u00f3", + "admin_username": "Rendszergazda Felhaszn\u00e1l\u00f3n\u00e9v", + "surveillance_password": "Fel\u00fcgyelet Jelsz\u00f3", + "surveillance_username": "Fel\u00fcgyelet Felhaszn\u00e1l\u00f3n\u00e9v", + "url": "URL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "\u00c1ll\u00edtsa be a motionEye webhookokat az esem\u00e9nyek jelent\u00e9s\u00e9nek a Home Assistant sz\u00e1m\u00e1ra", + "webhook_set_overwrite": "Fel\u00fcl\u00edrja a fel nem ismert webhookokat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index f265789d777..84c4a40f082 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -60,6 +60,9 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } + }, + "options": { + "title": "MQTT opci\u00f3k" } } } diff --git a/homeassistant/components/mullvad/translations/hu.json b/homeassistant/components/mullvad/translations/hu.json index e92d5c4bdea..aedebce2afc 100644 --- a/homeassistant/components/mullvad/translations/hu.json +++ b/homeassistant/components/mullvad/translations/hu.json @@ -6,6 +6,11 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "description": "Be\u00e1ll\u00edtja a Mullvad VPN integr\u00e1ci\u00f3t?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/hu.json b/homeassistant/components/mutesync/translations/hu.json new file mode 100644 index 00000000000..68cb5c18d27 --- /dev/null +++ b/homeassistant/components/mutesync/translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "Enged\u00e9lyezze a hiteles\u00edt\u00e9st a m\u00fctesync be\u00e1ll\u00edt\u00e1sai > Hiteles\u00edt\u00e9s men\u00fcpontban", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "Gazdag\u00e9p" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json index 9c5b90e7447..59338cf43ae 100644 --- a/homeassistant/components/myq/translations/hu.json +++ b/homeassistant/components/myq/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A(z) {username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", + "title": "Hiteles\u00edtse \u00fajra MyQ-fi\u00f3kj\u00e1t" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index fefe3fd4b6c..e9d9caaeb4f 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -3,36 +3,76 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "duplicate_persistence_file": "A perzisztencia f\u00e1jl m\u00e1r haszn\u00e1latban van", + "duplicate_topic": "A t\u00e9ma m\u00e1r haszn\u00e1latban van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", + "invalid_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm", + "invalid_persistence_file": "\u00c9rv\u00e9nytelen perzisztencia f\u00e1jl", + "invalid_port": "\u00c9rv\u00e9nytelen portsz\u00e1m", + "invalid_publish_topic": "\u00c9rv\u00e9nytelen k\u00f6zz\u00e9t\u00e9teli t\u00e9ma", + "invalid_serial": "\u00c9rv\u00e9nytelen soros port", + "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si t\u00e9ma", + "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", "not_a_number": "Adj meg egy sz\u00e1mot.", + "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", + "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "duplicate_persistence_file": "A perzisztencia f\u00e1jl m\u00e1r haszn\u00e1latban van", + "duplicate_topic": "A t\u00e9ma m\u00e1r haszn\u00e1latban van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", + "invalid_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm", + "invalid_persistence_file": "\u00c9rv\u00e9nytelen perzisztencia f\u00e1jl", + "invalid_port": "\u00c9rv\u00e9nytelen portsz\u00e1m", + "invalid_publish_topic": "\u00c9rv\u00e9nytelen k\u00f6zz\u00e9t\u00e9teli t\u00e9ma", "invalid_serial": "\u00c9rv\u00e9nytelen soros port", + "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si t\u00e9ma", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", + "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", + "not_a_number": "K\u00e9rj\u00fck, adja meg a sz\u00e1mot", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", + "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "gw_mqtt": { "data": { + "persistence_file": "perzisztencia f\u00e1jl (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", + "retain": "mqtt megtart\u00e1sa", + "topic_in_prefix": "el\u0151tag a beviteli t\u00e9m\u00e1khoz (topic_in_prefix)", + "topic_out_prefix": "el\u0151tag a kimeneti t\u00e9m\u00e1khoz (topic_out_prefix)", "version": "MySensors verzi\u00f3" - } + }, + "description": "MQTT \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa" }, "gw_serial": { "data": { + "baud_rate": "\u00e1tviteli sebess\u00e9g", + "device": "Soros port", + "persistence_file": "perzisztencia f\u00e1jl (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", "version": "MySensors verzi\u00f3" - } + }, + "description": "Soros \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa" }, "gw_tcp": { "data": { + "device": "Az \u00e1tj\u00e1r\u00f3 IP-c\u00edme", + "persistence_file": "perzisztencia f\u00e1jl (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", "tcp_port": "port", "version": "MySensors verzi\u00f3" - } + }, + "description": "Ethernet \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa" + }, + "user": { + "data": { + "gateway_type": "\u00c1tj\u00e1r\u00f3 t\u00edpusa" + }, + "description": "V\u00e1lassza ki az \u00e1tj\u00e1r\u00f3hoz val\u00f3 csatlakoz\u00e1si m\u00f3dot" } } }, diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json new file mode 100644 index 00000000000..8776ae92e20 --- /dev/null +++ b/homeassistant/components/nam/translations/hu.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "device_unsupported": "Az eszk\u00f6z nem t\u00e1mogatott." + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "unknown": "V\u00e1ratlan hiba" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Nettigo Air Monitor-ot a {host} c\u00edmen?" + }, + "user": { + "data": { + "host": "Gazdag\u00e9p" + }, + "description": "\u00c1ll\u00edtsa be a Nettigo Air Monitor integr\u00e1ci\u00f3j\u00e1t." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index b4979396eeb..0e6536bb0ad 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -17,7 +17,9 @@ }, "device_automation": { "trigger_subtype": { - "away": "t\u00e1vol" + "away": "t\u00e1vol", + "hg": "fagyv\u00e9d\u0151", + "schedule": "\u00fctemez" }, "trigger_type": { "alarm_started": "{entity_name} riaszt\u00e1st \u00e9szlelt", diff --git a/homeassistant/components/nexia/translations/hu.json b/homeassistant/components/nexia/translations/hu.json index 7dedf459484..ac85fec6456 100644 --- a/homeassistant/components/nexia/translations/hu.json +++ b/homeassistant/components/nexia/translations/hu.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "M\u00e1rka", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json index 459a879e82c..b3e5a36e172 100644 --- a/homeassistant/components/nightscout/translations/hu.json +++ b/homeassistant/components/nightscout/translations/hu.json @@ -14,7 +14,9 @@ "data": { "api_key": "API kulcs", "url": "URL" - } + }, + "description": "- URL: a nightcout p\u00e9ld\u00e1ny c\u00edme. Vagyis: https://myhomeassistant.duckdns.org:5423\n - API kulcs (opcion\u00e1lis): Csak akkor haszn\u00e1lja, ha a p\u00e9ld\u00e1nya v\u00e9dett (auth_default_roles! = Olvashat\u00f3).", + "title": "Adja meg a Nightscout szerver adatait." } } } diff --git a/homeassistant/components/nmap_tracker/translations/hu.json b/homeassistant/components/nmap_tracker/translations/hu.json new file mode 100644 index 00000000000..1b5dc9d029b --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/hu.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_hosts": "\u00c9rv\u00e9nytelen gazdag\u00e9pek" + }, + "step": { + "user": { + "data": { + "exclude": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva), amelyeket kiz\u00e1r\u00e1sra ker\u00fclnek a vizsg\u00e1latb\u00f3l", + "home_interval": "Minim\u00e1lis percsz\u00e1m az akt\u00edv eszk\u00f6z\u00f6k vizsg\u00e1lata k\u00f6z\u00f6tt (akkumul\u00e1tor k\u00edm\u00e9l\u00e9se)", + "hosts": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva) a beolvas\u00e1shoz", + "scan_options": "Nyersen konfigur\u00e1lhat\u00f3 szkennel\u00e9si lehet\u0151s\u00e9gek az Nmap sz\u00e1m\u00e1ra" + }, + "description": "\u00c1ll\u00edtsa be a gazdag\u00e9peket, hogy az Nmap ellen\u0151rizhesse \u0151ket. A h\u00e1l\u00f3zati c\u00edm IP-c\u00edm (192.168.1.1), IP-h\u00e1l\u00f3zat (192.168.0.0/24) vagy IP-tartom\u00e1ny (192.168.1.0-32) lehet." + } + } + }, + "options": { + "error": { + "invalid_hosts": "\u00c9rv\u00e9nytelen gazdag\u00e9p" + }, + "step": { + "init": { + "data": { + "exclude": "A szkennel\u00e9sb\u0151l kiz\u00e1rand\u00f3 h\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva)", + "home_interval": "Minim\u00e1lis percsz\u00e1m az akt\u00edv eszk\u00f6z\u00f6k vizsg\u00e1lata k\u00f6z\u00f6tt (akkumul\u00e1tor k\u00edm\u00e9l\u00e9se)", + "hosts": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva) a beolvas\u00e1shoz", + "interval_seconds": "Szkennel\u00e9si intervallum", + "scan_options": "Nyersen konfigur\u00e1lhat\u00f3 beolvas\u00e1si lehet\u0151s\u00e9gek az Nmap sz\u00e1m\u00e1ra", + "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se" + }, + "description": "\u00c1ll\u00edtsa be a gazdag\u00e9peket, amelyeket a Nmap ellen\u0151riz. A h\u00e1l\u00f3zati c\u00edm IP-c\u00edm (192.168.1.1), IP-h\u00e1l\u00f3zat (192.168.0.0/24) vagy IP-tartom\u00e1ny (192.168.1.0-32) lehet." + } + } + }, + "title": "Nmap k\u00f6vet\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/hu.json b/homeassistant/components/nuki/translations/hu.json index 4f0b1a29738..7a0b6b6159e 100644 --- a/homeassistant/components/nuki/translations/hu.json +++ b/homeassistant/components/nuki/translations/hu.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "token": "Hozz\u00e1f\u00e9r\u00e9si token" + }, + "description": "A Nuki integr\u00e1ci\u00f3nak \u00fajb\u00f3l hiteles\u00edtenie kell a h\u00edddal.", + "title": "Az integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" + }, "user": { "data": { "host": "Hoszt", diff --git a/homeassistant/components/omnilogic/translations/hu.json b/homeassistant/components/omnilogic/translations/hu.json index 129bb041b42..a0a1facddd5 100644 --- a/homeassistant/components/omnilogic/translations/hu.json +++ b/homeassistant/components/omnilogic/translations/hu.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH-eltol\u00e1s (pozit\u00edv vagy negat\u00edv)", "polling_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (m\u00e1sodpercben)" } } diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index 662475dde2c..e2c7ffa8c03 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -12,7 +12,8 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "Owserver adatok be\u00e1ll\u00edt\u00e1sa" }, "user": { "data": { diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index 61a6cfb056e..e2b63a6c9d8 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -18,6 +18,16 @@ }, "title": "Hiteles\u00edt\u00e9s konfigur\u00e1l\u00e1sa" }, + "configure": { + "data": { + "host": "Gazdag\u00e9p", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Konfigur\u00e1lja az ONVIF eszk\u00f6zt" + }, "configure_profile": { "data": { "include": "Hozzon l\u00e9tre kamera entit\u00e1st" @@ -40,6 +50,9 @@ "title": "ONVIF eszk\u00f6z konfigur\u00e1l\u00e1sa" }, "user": { + "data": { + "auto": "Automatikus keres\u00e9s" + }, "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban." } } diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 78ff8c88636..77112bd8929 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -21,6 +21,8 @@ "init": { "data": { "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", + "read_precision": "Pontoss\u00e1g olvas\u00e1sa", + "set_precision": "Pontoss\u00e1g be\u00e1ll\u00edt\u00e1sa", "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" } } diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 14b3b23b2e7..143d1a8dc18 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -11,6 +11,7 @@ "data": { "password": "Jelsz\u00f3" }, + "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index 6c2c6c22f55..70934bf3472 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -11,7 +11,16 @@ "error": { "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." }, + "progress": { + "install_addon": "V\u00e1rjon, am\u00edg az OpenZWave kieg\u00e9sz\u00edt\u0151 telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat." + }, "step": { + "hassio_confirm": { + "title": "\u00c1ll\u00edtsa be az OpenZWave integr\u00e1ci\u00f3t az OpenZWave kieg\u00e9sz\u00edt\u0151vel" + }, + "install_addon": { + "title": "Elindult az OpenZWave kieg\u00e9sz\u00edt\u0151 telep\u00edt\u00e9se" + }, "on_supervisor": { "data": { "use_addon": "Haszn\u00e1ld az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt" @@ -23,7 +32,8 @@ "data": { "network_key": "H\u00e1l\u00f3zati kulcs", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - } + }, + "title": "Adja meg az OpenZWave b\u0151v\u00edtm\u00e9ny konfigur\u00e1ci\u00f3j\u00e1t" } } } diff --git a/homeassistant/components/philips_js/translations/hu.json b/homeassistant/components/philips_js/translations/hu.json index f7ce3f708b0..1fe4811d21e 100644 --- a/homeassistant/components/philips_js/translations/hu.json +++ b/homeassistant/components/philips_js/translations/hu.json @@ -24,5 +24,19 @@ } } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Az eszk\u00f6znek be kell lennie kapcsolva" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Adat\u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s haszn\u00e1lat\u00e1nak enged\u00e9lyez\u00e9se." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/hu.json b/homeassistant/components/picnic/translations/hu.json new file mode 100644 index 00000000000..c70dcca0260 --- /dev/null +++ b/homeassistant/components/picnic/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "country_code": "Orsz\u00e1g k\u00f3d", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "title": "Piknik" +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 8347b5d2f98..4778b41e8be 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -8,7 +8,20 @@ "create_entry": { "default": "A Plaato {device_type} **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" }, + "error": { + "invalid_webhook_device": "Olyan eszk\u00f6zt v\u00e1lasztott, amely nem t\u00e1mogatja az adatok webhookra t\u00f6rt\u00e9n\u0151 k\u00fcld\u00e9s\u00e9t. Csak az Airlock sz\u00e1m\u00e1ra \u00e9rhet\u0151 el", + "no_api_method": "Hozz\u00e1 kell adnia egy hiteles\u00edt\u00e9si tokent, vagy ki kell v\u00e1lasztania a webhookot", + "no_auth_token": "Hozz\u00e1 kell adnia egy hiteles\u00edt\u00e9si tokent" + }, "step": { + "api_method": { + "data": { + "token": "Auth Token beilleszt\u00e9se ide", + "use_webhook": "Webhook haszn\u00e1lata" + }, + "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\n Kiv\u00e1lasztott eszk\u00f6z: ** {device_type} ** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rj\u00fck, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", + "title": "API m\u00f3dszer kiv\u00e1laszt\u00e1sa" + }, "user": { "data": { "device_name": "Eszk\u00f6z neve", @@ -16,6 +29,25 @@ }, "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "A Plaato eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" + }, + "webhook": { + "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} ).", + "title": "Haszn\u00e1land\u00f3 webhook" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Friss\u00edt\u00e9si intervallum (perc)" + }, + "description": "A friss\u00edt\u00e9si id\u0151k\u00f6z be\u00e1ll\u00edt\u00e1sa (percben)", + "title": "Opci\u00f3k a Plaato sz\u00e1m\u00e1ra" + }, + "webhook": { + "description": "Webhook inform\u00e1ci\u00f3k: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n", + "title": "Opci\u00f3k a Plaato Airlock-hoz" } } } diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 3d7de972fb0..4d588c84277 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -28,5 +28,15 @@ "title": "Csatlakoz\u00e1s a Smile-hoz" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" + }, + "description": "\u00c1ll\u00edtsa be a Plugwise lehet\u0151s\u00e9get" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index 1fc46d0c19b..17dc73a189b 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -3,6 +3,7 @@ "abort": { "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "external_setup": "A pont sikeresen konfigur\u00e1lva van egy m\u00e1sik folyamatb\u00f3l.", "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json index f6f6e2c15b7..76af6fb124f 100644 --- a/homeassistant/components/progettihwsw/translations/hu.json +++ b/homeassistant/components/progettihwsw/translations/hu.json @@ -19,8 +19,15 @@ "relay_15": "Rel\u00e9 15", "relay_16": "Rel\u00e9 16", "relay_2": "Rel\u00e9 2", - "relay_3": "Rel\u00e9 3" - } + "relay_3": "Rel\u00e9 3", + "relay_4": "4-es rel\u00e9", + "relay_5": "5-\u00f6s rel\u00e9 ", + "relay_6": "6-os rel\u00e9", + "relay_7": "7-es rel\u00e9", + "relay_8": "8-as rel\u00e9", + "relay_9": "9-es rel\u00e9" + }, + "title": "Rel\u00e9k be\u00e1ll\u00edt\u00e1sa" }, "user": { "data": { diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json index bb677b21700..97614bcac57 100644 --- a/homeassistant/components/ps4/translations/hu.json +++ b/homeassistant/components/ps4/translations/hu.json @@ -2,14 +2,20 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + "credential_error": "Hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u0151 adatok beolvas\u00e1sa sor\u00e1n.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "port_987_bind_error": "Nem siker\u00fclt a 987. porthoz kapcsol\u00f3dni. Tov\u00e1bbi inform\u00e1ci\u00f3 a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3 (https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "Nem siker\u00fclt a 997-es porthoz kapcsol\u00f3dni. Tov\u00e1bbi inform\u00e1ci\u00f3 a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3 (https://www.home-assistant.io/components/ps4/)." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a PIN-k\u00f3d helyes-e." + "credential_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s. Az \u00fajraind\u00edt\u00e1shoz nyomja meg a bek\u00fcld\u00e9s gombot.", + "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a PIN-k\u00f3d helyes-e.", + "no_ipaddress": "\u00cdrja be a konfigur\u00e1lni k\u00edv\u00e1nt PlayStation 4 IP c\u00edm\u00e9t" }, "step": { "creds": { + "description": "Hiteles\u00edt\u0151 adatok sz\u00fcks\u00e9gesek. Nyomja meg a \u201eK\u00fcld\u00e9s\u201d gombot, majd a PS4 2. k\u00e9perny\u0151 alkalmaz\u00e1sban friss\u00edtse az eszk\u00f6z\u00f6ket, \u00e9s a folytat\u00e1shoz v\u00e1lassza a \u201eHome-Assistant\u201d eszk\u00f6zt.", "title": "PlayStation 4" }, "link": { @@ -18,13 +24,16 @@ "ip_address": "IP c\u00edm", "name": "N\u00e9v", "region": "R\u00e9gi\u00f3" - } + }, + "description": "Adja meg a PlayStation 4 adatait. A PIN-k\u00f3d eset\u00e9ben keresse meg a PlayStation 4 konzol \u201eBe\u00e1ll\u00edt\u00e1sok\u201d elem\u00e9t. Ezut\u00e1n keresse meg a \u201eMobilalkalmaz\u00e1s-kapcsolat be\u00e1ll\u00edt\u00e1sai\u201d elemet, \u00e9s v\u00e1lassza az \u201eEszk\u00f6z hozz\u00e1ad\u00e1sa\u201d lehet\u0151s\u00e9get. \u00cdrja be a megjelen\u0151 PIN-k\u00f3d . Tov\u00e1bbi inform\u00e1ci\u00f3k a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3k (https://www.home-assistant.io/components/ps4/).", + "title": "PlayStation 4" }, "mode": { "data": { "ip_address": "IP c\u00edm (Hagyd \u00fcresen az Automatikus Felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz).", "mode": "Konfigur\u00e1ci\u00f3s m\u00f3d" }, + "description": "V\u00e1lassza ki a m\u00f3dot a konfigur\u00e1l\u00e1shoz. Az IP c\u00edm mez\u0151 \u00fcresen maradhat, ha az Automatikus felder\u00edt\u00e9s lehet\u0151s\u00e9get v\u00e1lasztja, mivel az eszk\u00f6z\u00f6k automatikusan felfedez\u0151dnek.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index 17bca647c18..0b980bd58e0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -2,11 +2,26 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", + "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)" + }, + "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)." + } } }, "options": { "step": { "init": { + "data": { + "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", + "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)", + "tariff": "Alkalmazand\u00f3 tarifa f\u00f6ldrajzi z\u00f3n\u00e1k szerint" + }, + "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/recollect_waste/translations/hu.json b/homeassistant/components/recollect_waste/translations/hu.json index 3222f50be02..4c570b8d9de 100644 --- a/homeassistant/components/recollect_waste/translations/hu.json +++ b/homeassistant/components/recollect_waste/translations/hu.json @@ -18,6 +18,9 @@ "options": { "step": { "init": { + "data": { + "friendly_name": "Bar\u00e1ts\u00e1gos nevek haszn\u00e1lata a felv\u00e9teli t\u00edpusok eset\u00e9ben (ha lehets\u00e9ges)" + }, "title": "Recollect Waste konfigur\u00e1l\u00e1sa" } } diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 20ef3db6171..d8a27a3173b 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -5,14 +5,19 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "one": "\u00dcres", + "other": "\u00dcres" }, "step": { + "one": "\u00dcres", + "other": "\u00dcres", "setup_network": { "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "V\u00e1lassza ki a csatlakoz\u00e1si c\u00edmet" }, "setup_serial": { "data": { @@ -25,22 +30,50 @@ "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, "title": "El\u00e9r\u00e9si \u00fat" + }, + "user": { + "data": { + "type": "Kapcsolat t\u00edpusa" + }, + "title": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t" } } }, + "one": "\u00dcres", "options": { "error": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d", + "invalid_input_2262_off": "\u00c9rv\u00e9nytelen bemenet a kikapcsol\u00e1si parancshoz", + "invalid_input_2262_on": "\u00c9rv\u00e9nytelen bemenet a parancshoz", + "invalid_input_off_delay": "\u00c9rv\u00e9nytelen bemenet a kikapcsol\u00e1si k\u00e9sleltet\u00e9shez", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "prompt_options": { "data": { + "automatic_add": "Enged\u00e9lyezze az automatikus hozz\u00e1ad\u00e1st", + "debug": "Enged\u00e9lyezze a hibakeres\u00e9st", + "device": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt", + "event_code": "\u00cdrja be a hozz\u00e1adni k\u00edv\u00e1nt esem\u00e9ny k\u00f3dj\u00e1t", "remove_device": "V\u00e1lassza ki a t\u00f6r\u00f6lni k\u00edv\u00e1nt eszk\u00f6zt" }, "title": "Rfxtrx opci\u00f3k" + }, + "set_device_options": { + "data": { + "command_off": "Adatbitek \u00e9rt\u00e9ke a parancs kikapcsol\u00e1s\u00e1hoz", + "command_on": "Adatbitek \u00e9rt\u00e9ke a parancshoz", + "data_bit": "Adatbitek sz\u00e1ma", + "fire_event": "Eszk\u00f6zesem\u00e9ny enged\u00e9lyez\u00e9se", + "off_delay": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s", + "off_delay_enabled": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s enged\u00e9lyez\u00e9se", + "replace_device": "V\u00e1lassza ki a cser\u00e9lni k\u00edv\u00e1nt eszk\u00f6zt", + "signal_repetitions": "A jelism\u00e9tl\u00e9sek sz\u00e1ma" + }, + "title": "Konfigur\u00e1lja az eszk\u00f6z be\u00e1ll\u00edt\u00e1sait" } } - } + }, + "other": "\u00dcres" } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json index ee57b9488dc..aaa7974cd4a 100644 --- a/homeassistant/components/risco/translations/hu.json +++ b/homeassistant/components/risco/translations/hu.json @@ -20,8 +20,35 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u00c9les\u00edtve t\u00e1vol", + "armed_custom_bypass": "\u00c9les\u00edtve (egy\u00e9ni)", + "armed_home": "\u00c9les\u00edtve (otthon)", + "armed_night": "\u00c9les\u00edtve (\u00e9jszakai)" + }, + "description": "V\u00e1lassza ki, hogy milyen \u00e1llapotba \u00e1ll\u00edtsa a Risco riaszt\u00e1st a Home Assistant riaszt\u00e1s \u00e9les\u00edt\u00e9sekor", + "title": "A Home Assistant \u00e1llapotok megjelen\u00edt\u00e9se Risco \u00e1llapotokba" + }, "init": { + "data": { + "code_arm_required": "Az \u00e9les\u00edt\u00e9shez PIN-k\u00f3d sz\u00fcks\u00e9ges", + "code_disarm_required": "A hat\u00e1stalan\u00edt\u00e1shoz a PIN-k\u00f3d sz\u00fcks\u00e9ges", + "scan_interval": "Milyen gyakran kell lek\u00e9rdezni Risco-t (m\u00e1sodpercben)" + }, "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" + }, + "risco_to_ha": { + "data": { + "A": "A csoport", + "B": "B csoport", + "C": "C csoport", + "D": "D csoport", + "arm": "\u00c9les\u00edtve (t\u00e1voll\u00e9t)", + "partial_arm": "R\u00e9szben \u00e9les\u00edtve (otthon)" + }, + "description": "V\u00e1lassza ki, hogy a Home Assistant riaszt\u00e1sa milyen \u00e1llapotot jelent a Risco \u00e1ltal jelentett minden \u00e1llapotr\u00f3l", + "title": "A Risco \u00e1llapotok hozz\u00e1rendel\u00e9se Home Assistant \u00e1llapotokhoz" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/hu.json b/homeassistant/components/rituals_perfume_genie/translations/hu.json index 4ecaf2ba0d0..4c3cdfeae86 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/hu.json +++ b/homeassistant/components/rituals_perfume_genie/translations/hu.json @@ -13,7 +13,8 @@ "data": { "email": "E-mail", "password": "Jelsz\u00f3" - } + }, + "title": "Csatlakozzon Rituals-fi\u00f3kj\u00e1hoz" } } } diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 51957ba8847..2f8d902f4fe 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -15,12 +15,18 @@ "data": { "host": "Hoszt" }, + "description": "V\u00e1lasszon egy Roomba vagy Braava k\u00e9sz\u00fcl\u00e9ket.", "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" }, + "link": { + "description": "Nyomja meg \u00e9s tartsa lenyomva a(z) {name} Kezd\u0151lap (Home) gombot, am\u00edg az eszk\u00f6z hangot ad (kb. K\u00e9t m\u00e1sodperc), majd engedje el 30 m\u00e1sodpercen bel\u00fcl.", + "title": "Jelsz\u00f3 lek\u00e9r\u00e9se" + }, "link_manual": { "data": { "password": "Jelsz\u00f3" }, + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { @@ -28,6 +34,7 @@ "blid": "BLID", "host": "Hoszt" }, + "description": "A h\u00e1l\u00f3zaton egyetlen Roomba vagy Braava sem ker\u00fclt el\u0151. A BLID az eszk\u00f6z hostnev\u00e9nek az `iRobot-` vagy `Roomba -` ut\u00e1ni r\u00e9sze. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", "title": "Manu\u00e1lis csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" }, "user": { diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index f05fd838572..123027a8216 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -8,6 +8,9 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "link": { + "title": "Enged\u00e9lyezze a HomeAssistant alkalmaz\u00e1st Roon-ban" + }, "user": { "data": { "host": "Hoszt" diff --git a/homeassistant/components/rpi_power/translations/hu.json b/homeassistant/components/rpi_power/translations/hu.json index 2d1c0811286..feb1687037f 100644 --- a/homeassistant/components/rpi_power/translations/hu.json +++ b/homeassistant/components/rpi_power/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "Nem tal\u00e1lja az ehhez a komponenshez sz\u00fcks\u00e9ges rendszerszintet, gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a rendszermag (kernel) friss, \u00e9s a hardver t\u00e1mogatott", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index a720c5932ed..f0aa85433a1 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -5,6 +5,7 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "id_missing": "Ennek a Samsung eszk\u00f6znek nincs sorsz\u00e1ma.", "not_supported": "Ez a Samsung k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" @@ -18,6 +19,9 @@ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {device} k\u00e9sz\u00fcl\u00e9ket? Ha kor\u00e1bban m\u00e9g csatlakoztattad a Home Assistantet, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "A bek\u00fcld\u00e9s ut\u00e1n fogadja el a(z) {device} felugr\u00f3 ablakot, amely 30 m\u00e1sodpercen bel\u00fcl enged\u00e9lyt k\u00e9r." + }, "user": { "data": { "host": "Hoszt", diff --git a/homeassistant/components/screenlogic/translations/hu.json b/homeassistant/components/screenlogic/translations/hu.json index f46ab499a29..3efb8ec6e60 100644 --- a/homeassistant/components/screenlogic/translations/hu.json +++ b/homeassistant/components/screenlogic/translations/hu.json @@ -20,6 +20,7 @@ "data": { "selected_gateway": "Gateway" }, + "description": "A k\u00f6vetkez\u0151 ScreenLogic \u00e1tj\u00e1r\u00f3kat fedezt\u00e9k fel. V\u00e1lasszon egyet a konfigur\u00e1l\u00e1shoz, vagy v\u00e1lassza a ScreenLogic \u00e1tj\u00e1r\u00f3 k\u00e9zi konfigur\u00e1l\u00e1s\u00e1t.", "title": "ScreenLogic" } } diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index beb1c56b305..9b1c9bece82 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -4,21 +4,28 @@ "is_battery_level": "{entity_name} aktu\u00e1lis akku szintje", "is_carbon_dioxide": "Jelenlegi {entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3 szint", "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint", + "is_current": "Jelenlegi {entity_name} \u00e1ram", + "is_energy": "A jelenlegi {entity_name} energia", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", + "is_power_factor": "A jelenlegi {entity_name} teljes\u00edtm\u00e9nyt\u00e9nyez\u0151", "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", - "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke" + "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke", + "is_voltage": "A jelenlegi {entity_name} fesz\u00fclts\u00e9g" }, "trigger_type": { "battery_level": "{entity_name} akku szintje v\u00e1ltozik", "carbon_dioxide": "{entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", + "current": "{entity_name} aktu\u00e1lis v\u00e1ltoz\u00e1sai", + "energy": "{entity_name} energiav\u00e1ltoz\u00e1sa", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", + "power_factor": "{entity_name} teljes\u00edtm\u00e9nyt\u00e9nyez\u0151 megv\u00e1ltozik", "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 055c8817177..79188df18b1 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -9,9 +9,25 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Add meg a Sentry DSN-t", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "A k\u00f6rnyezet v\u00e1laszthat\u00f3 neve.", + "event_custom_components": "Esem\u00e9nyek k\u00fcld\u00e9se egy\u00e9ni \u00f6sszetev\u0151kb\u0151l", + "event_handled": "K\u00fcldj\u00f6n kezelt esem\u00e9nyeket", + "event_third_party_packages": "K\u00fcldj\u00f6n esem\u00e9nyeket harmadik f\u00e9l csomagjaib\u00f3l", + "tracing": "Enged\u00e9lyezze a teljes\u00edtm\u00e9nyk\u00f6vet\u00e9st" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json index f5538bfd6b5..6a5c609e1f6 100644 --- a/homeassistant/components/sia/translations/hu.json +++ b/homeassistant/components/sia/translations/hu.json @@ -1,15 +1,50 @@ { "config": { "error": { + "invalid_account_format": "A sz\u00e1mla nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_account_length": "A fi\u00f3k nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 3 \u00e9s 16 karakter k\u00f6z\u00f6tt kell lennie.", + "invalid_key_format": "A kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_key_length": "A kulcs nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 16, 24 vagy 32 hexa karakterb\u0151l kell \u00e1llnia.", + "invalid_ping": "A ping intervallumnak 1 \u00e9s 1440 perc k\u00f6z\u00f6tt kell lennie.", + "invalid_zones": "Legal\u00e1bb 1 z\u00f3n\u00e1nak kell lennie.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "additional_account": { + "data": { + "account": "Fi\u00f3k ID", + "additional_account": "Tov\u00e1bbi fi\u00f3kok", + "encryption_key": "Titkos\u00edt\u00e1si kulcs", + "ping_interval": "Ping-intervallum (perc)", + "zones": "A fi\u00f3k z\u00f3n\u00e1inak sz\u00e1ma" + }, + "title": "Adjon hozz\u00e1 egy m\u00e1sik fi\u00f3kot az aktu\u00e1lis porthoz." + }, "user": { "data": { + "account": "Fi\u00f3k ID", + "additional_account": "Tov\u00e1bbi fi\u00f3kok", + "encryption_key": "Titkos\u00edt\u00e1si kulcs", + "ping_interval": "Ping-intervallum (perc)", "port": "Port", - "protocol": "Protokoll" - } + "protocol": "Protokoll", + "zones": "A fi\u00f3k z\u00f3n\u00e1inak sz\u00e1ma" + }, + "title": "Hozzon l\u00e9tre kapcsolatot SIA alap\u00fa riaszt\u00f3rendszerekhez." } } - } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Hagyja figyelmen k\u00edv\u00fcl a SIA esem\u00e9nyek id\u0151b\u00e9lyeg-ellen\u0151rz\u00e9s\u00e9t", + "zones": "A fi\u00f3k z\u00f3n\u00e1inak sz\u00e1ma" + }, + "description": "\u00c1ll\u00edtsa be a fi\u00f3k be\u00e1ll\u00edt\u00e1sait: {account}", + "title": "A SIA be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gek." + } + } + }, + "title": "SIA riaszt\u00f3rendszerek" } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/hu.json b/homeassistant/components/sma/translations/hu.json new file mode 100644 index 00000000000..cab063cd077 --- /dev/null +++ b/homeassistant/components/sma/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "cannot_retrieve_device_info": "Sikeres csatlakoz\u00e1s, de nem tudja lek\u00e9rni az eszk\u00f6z adatait", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "group": "Csoport", + "host": "Gazdag\u00e9p", + "password": "Jelsz\u00f3", + "ssl": "SSL tan\u00fas\u00edtv\u00e1nyt haszn\u00e1l", + "verify_ssl": "Ellen\u0151rizze az SSL tan\u00fas\u00edtv\u00e1nyt" + }, + "description": "Adja meg az SMA-eszk\u00f6z adatait.", + "title": "Az SMA Solar be\u00e1ll\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index c2535713626..5d3e65bb6fc 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -9,6 +9,11 @@ }, "flow_title": "{name}", "step": { + "environment": { + "data": { + "environment": "K\u00f6rnyezet" + } + }, "local": { "data": { "host": "Hoszt" diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 17c0a1a1b04..bd6808db322 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -20,6 +20,7 @@ } }, "user": { + "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", "title": "Callback URL meger\u0151s\u00edt\u00e9se" } } diff --git a/homeassistant/components/smarttub/translations/hu.json b/homeassistant/components/smarttub/translations/hu.json index a2a4a9d706e..e9a45d3773f 100644 --- a/homeassistant/components/smarttub/translations/hu.json +++ b/homeassistant/components/smarttub/translations/hu.json @@ -8,6 +8,10 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "reauth_confirm": { + "description": "A SmartTub integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t", + "title": "Az integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" + }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index 8479c90f595..69e450f55ff 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -5,6 +5,7 @@ }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "could_not_connect": "Nem siker\u00fclt csatlakozni a solaredge API-hoz", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", "site_not_active": "Az oldal nem akt\u00edv" }, diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 1cb4db9942a..5a2a1ee6ab5 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -13,8 +13,10 @@ "user": { "data": { "host": "Hoszt", - "port": "Port" - } + "port": "Port", + "system_id": "Rendszerazonos\u00edt\u00f3" + }, + "description": "A rendszerazonos\u00edt\u00f3t a MyLink alkalmaz\u00e1s Integr\u00e1ci\u00f3 r\u00e9sz\u00e9ben lehet beszerezni, b\u00e1rmely nem Cloud szolg\u00e1ltat\u00e1s kiv\u00e1laszt\u00e1s\u00e1val." } } }, @@ -24,15 +26,20 @@ }, "step": { "entity_config": { + "description": "Konfigur\u00e1lja az \u201e {entity_id} \u201d be\u00e1ll\u00edt\u00e1sait", "title": "Entit\u00e1s konfigur\u00e1l\u00e1sa" }, "init": { "data": { + "entity_id": "Konfigur\u00e1ljon egy adott entit\u00e1st.", "target_id": "Az \u00e1rny\u00e9kol\u00f3 be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa." }, "title": "Mylink be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" }, "target_config": { + "data": { + "reverse": "A bor\u00edt\u00f3 megfordult" + }, "description": "A(z) `{target_name}` be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa", "title": "MyLink \u00e1rny\u00e9kol\u00f3 konfigur\u00e1l\u00e1sa" } diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index 160f9685308..6ebdb22404c 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -12,6 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { + "description": "A Sonarr integr\u00e1ci\u00f3t manu\u00e1lisan kell hiteles\u00edteni a(z) {host}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/sonos/translations/hu.json b/homeassistant/components/sonos/translations/hu.json index 2123ec520f7..a928f97b3d6 100644 --- a/homeassistant/components/sonos/translations/hu.json +++ b/homeassistant/components/sonos/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_sonos_device": "A felfedezett eszk\u00f6z nem Sonos-eszk\u00f6z", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 060aeffe8bd..8ffeadaf842 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", - "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "reauth_account_mismatch": "A Spotify-fi\u00f3kkal hiteles\u00edtett fi\u00f3k nem egyezik meg az \u00faj hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges fi\u00f3kkal." }, "create_entry": { "default": "A Spotify sikeresen hiteles\u00edtett." @@ -13,6 +14,7 @@ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { + "description": "A Spotify integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a Spotify fi\u00f3kot: {account}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json index 0c3bdf29389..9ade185d831 100644 --- a/homeassistant/components/srp_energy/translations/hu.json +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_account": "A sz\u00e1mlaazonos\u00edt\u00f3nak 9 sz\u00e1mjegy\u0171nek kell lennie", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json index 3be1db59239..a54ddd57c39 100644 --- a/homeassistant/components/subaru/translations/hu.json +++ b/homeassistant/components/subaru/translations/hu.json @@ -15,6 +15,7 @@ "data": { "pin": "PIN" }, + "description": "K\u00e9rj\u00fck, adja meg MySubaru PIN-k\u00f3dj\u00e1t\n MEGJEGYZ\u00c9S: A sz\u00e1ml\u00e1n szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "user": { @@ -23,6 +24,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "K\u00e9rj\u00fck, adja meg MySubaru hiteles\u00edt\u0151 adatait\n MEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" } } @@ -30,6 +32,10 @@ "options": { "step": { "init": { + "data": { + "update_enabled": "Enged\u00e9lyezze a j\u00e1rm\u0171 lek\u00e9rdez\u00e9s\u00e9t" + }, + "description": "Ha enged\u00e9lyezve van, a j\u00e1rm\u0171 lek\u00e9rdez\u00e9se 2 \u00f3r\u00e1nk\u00e9nt t\u00e1voli parancsot k\u00fcld a j\u00e1rm\u0171v\u00e9nek \u00faj \u00e9rz\u00e9kel\u0151 adatok megszerz\u00e9s\u00e9hez. A j\u00e1rm\u0171 lek\u00e9rdez\u00e9se n\u00e9lk\u00fcl az \u00faj \u00e9rz\u00e9kel\u0151adatok csak akkor \u00e9rkeznek, amikor a j\u00e1rm\u0171 automatikusan tov\u00e1bb\u00edtja az adatokat (\u00e1ltal\u00e1ban a motor le\u00e1ll\u00edt\u00e1sa ut\u00e1n).", "title": "Subaru Starlink be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/syncthing/translations/hu.json b/homeassistant/components/syncthing/translations/hu.json new file mode 100644 index 00000000000..59ed4021b3f --- /dev/null +++ b/homeassistant/components/syncthing/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "title": "A szinkroniz\u00e1l\u00e1s integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa", + "token": "Token", + "url": "URL", + "verify_ssl": "Ellen\u0151rizze az SSL tan\u00fas\u00edtv\u00e1nyt" + } + } + } + }, + "title": "Szinkroniz\u00e1l\u00e1s" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index e5af260449a..7ac507f1efa 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -37,5 +37,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (m\u00e1sodperc)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index e8940bef26a..50643ca5e95 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -1,5 +1,32 @@ { "config": { - "flow_title": "{name}" - } + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "api_key": "API kulcs" + }, + "description": "K\u00e9rj\u00fck, adja meg a(z) {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott API-kulcsot." + }, + "user": { + "data": { + "api_key": "API kulcs", + "host": "Gazdag\u00e9p", + "port": "Port" + }, + "description": "K\u00e9rj\u00fck, adja meg kapcsolati adatait." + } + } + }, + "title": "Rendszer h\u00edd" } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json index 4461f2a2b71..72a26925bc9 100644 --- a/homeassistant/components/tasmota/translations/hu.json +++ b/homeassistant/components/tasmota/translations/hu.json @@ -3,8 +3,14 @@ "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "invalid_discovery_topic": "\u00c9rv\u00e9nytelen felfedez\u00e9si t\u00e9ma el\u0151tag." + }, "step": { "config": { + "data": { + "discovery_prefix": "Felder\u00edt\u00e9si t\u00e9ma el\u0151tagja" + }, "description": "Add meg a Tasmota konfigur\u00e1ci\u00f3t.", "title": "Tasmota" }, diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json index a496f1f2e45..207e9ada090 100644 --- a/homeassistant/components/tellduslive/translations/hu.json +++ b/homeassistant/components/tellduslive/translations/hu.json @@ -10,10 +10,15 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "auth": { + "description": "A TelldusLive-fi\u00f3k \u00f6sszekapcsol\u00e1sa:\n 1. Kattintson az al\u00e1bbi linkre\n 2. Jelentkezzen be a Telldus Live szolg\u00e1ltat\u00e1sba\n 3. Enged\u00e9lyezze ** {app_name} ** (kattintson a ** Yes ** gombra).\n 4. J\u00f6jj\u00f6n vissza ide, \u00e9s kattintson a ** SUBMIT ** gombra. \n\n [Link TelldusLive-fi\u00f3k] ( {auth_url} )", + "title": "Hiteles\u00edtsen a TelldusLive-on" + }, "user": { "data": { "host": "Hoszt" }, + "description": "\u00dcres", "title": "V\u00e1lassz v\u00e9gpontot." } } diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json index cd832522870..6371bf4c6fd 100644 --- a/homeassistant/components/toon/translations/hu.json +++ b/homeassistant/components/toon/translations/hu.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_agreements": "Ennek a fi\u00f3knak nincsenek Toon kijelz\u0151i.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." } diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index 6002f056635..e9e991d81d4 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -5,15 +5,20 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "usercode": "A felhaszn\u00e1l\u00f3i k\u00f3d nem \u00e9rv\u00e9nyes erre a felhaszn\u00e1l\u00f3ra ezen a helyen" }, "step": { "locations": { "data": { - "location": "Elhelyezked\u00e9s" - } + "location": "Elhelyezked\u00e9s", + "usercode": "Felhaszn\u00e1l\u00f3i k\u00f3d" + }, + "description": "Adja meg ennek a felhaszn\u00e1l\u00f3nak a felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1t a k\u00f6vetkez\u0151 helyen: {location_id}", + "title": "Helyhaszn\u00e1lati k\u00f3dok" }, "reauth_confirm": { + "description": "A Total Connectnek \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/tplink/translations/hu.json b/homeassistant/components/tplink/translations/hu.json index ab799e90c74..bcfb467538d 100644 --- a/homeassistant/components/tplink/translations/hu.json +++ b/homeassistant/components/tplink/translations/hu.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a TP-Link intelligens eszk\u00f6z\u00f6ket?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index b45148f80b8..054e6443d2a 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -40,8 +40,10 @@ "max_temp": "Maxim\u00e1lis c\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (haszn\u00e1lja a min-t \u00e9s a max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)", "min_kelvin": "Minimum t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", "min_temp": "Min. C\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (alap\u00e9rtelmez\u00e9s szerint haszn\u00e1ljon min-t \u00e9s max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)", + "set_temp_divided": "A h\u0151m\u00e9rs\u00e9klet be\u00e1ll\u00edt\u00e1s\u00e1hoz osztott h\u0151m\u00e9rs\u00e9kleti \u00e9rt\u00e9ket haszn\u00e1ljon", "support_color": "Sz\u00ednt\u00e1mogat\u00e1s k\u00e9nyszer\u00edt\u00e9se", "temp_divider": "Sz\u00ednh\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9kek oszt\u00f3ja (0 = alap\u00e9rtelmezett)", + "temp_step_override": "C\u00e9lh\u0151m\u00e9rs\u00e9klet l\u00e9pcs\u0151", "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet", "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g" }, diff --git a/homeassistant/components/twinkly/translations/hu.json b/homeassistant/components/twinkly/translations/hu.json index 190d7e469d5..d5cd872bbd0 100644 --- a/homeassistant/components/twinkly/translations/hu.json +++ b/homeassistant/components/twinkly/translations/hu.json @@ -8,6 +8,10 @@ }, "step": { "user": { + "data": { + "host": "A Twinkly eszk\u00f6z gazdag\u00e9pe (vagy IP-c\u00edme)" + }, + "description": "\u00c1ll\u00edtsa be a Twinkly led-karakterl\u00e1nc\u00e1t", "title": "Twinkly" } } diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 745b628b253..5c174e9939d 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "A vez\u00e9rl\u0151 webhelye m\u00e1r konfigur\u00e1lva van", "configuration_updated": "A konfigur\u00e1ci\u00f3 friss\u00edtve.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, @@ -26,6 +27,9 @@ "options": { "step": { "client_control": { + "data": { + "dpi_restrictions": "Enged\u00e9lyezze a DPI restrikci\u00f3s csoportok vez\u00e9rl\u00e9s\u00e9t" + }, "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st." }, "simple_options": { @@ -33,7 +37,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" + "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra", + "allow_uptime_sensors": "\u00dczemid\u0151-\u00e9rz\u00e9kel\u0151k h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" } } } diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 0bffeeaf154..49756babc8b 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -12,9 +12,19 @@ "step": { "user": { "data": { + "unique_id": "Eszk\u00f6z", "usn": "Eszk\u00f6z" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si intervallum (m\u00e1sodperc, minimum 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/ar.json b/homeassistant/components/vacuum/translations/ar.json index 2e9d6c9a5d6..630b54d4676 100644 --- a/homeassistant/components/vacuum/translations/ar.json +++ b/homeassistant/components/vacuum/translations/ar.json @@ -3,6 +3,7 @@ "_": { "cleaning": "\u062a\u0646\u0638\u064a\u0641", "error": "\u062e\u0637\u0623", + "idle": "\u062e\u0627\u0645\u0644", "off": "\u0645\u0637\u0641\u0626", "on": "\u0645\u0634\u063a\u0644", "paused": "\u0645\u0648\u0642\u0651\u0641 \u0645\u0624\u0642\u062a\u0627", diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index 85e53003566..f071872b81c 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -12,7 +12,8 @@ "installation": { "data": { "giid": "Telep\u00edt\u00e9s" - } + }, + "description": "A Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kodban. K\u00e9rj\u00fck, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1n adni a Home Assistant programhoz." }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/wallbox/translations/hu.json b/homeassistant/components/wallbox/translations/hu.json index fd8db27da5e..097ba53f02e 100644 --- a/homeassistant/components/wallbox/translations/hu.json +++ b/homeassistant/components/wallbox/translations/hu.json @@ -12,9 +12,11 @@ "user": { "data": { "password": "Jelsz\u00f3", + "station": "\u00c1llom\u00e1s sorozatsz\u00e1ma", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } - } + }, + "title": "Wallbox" } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/hu.json b/homeassistant/components/waze_travel_time/translations/hu.json index 94e5f96814e..401eb3c814c 100644 --- a/homeassistant/components/waze_travel_time/translations/hu.json +++ b/homeassistant/components/waze_travel_time/translations/hu.json @@ -10,9 +10,11 @@ "user": { "data": { "destination": "\u00c9rkez\u00e9s helye", + "name": "N\u00e9v", "origin": "Indul\u00e1s helye", "region": "R\u00e9gi\u00f3" - } + }, + "description": "Az Origin and Destination mez\u0151be \u00edrja be a hely c\u00edm\u00e9t vagy GPS koordin\u00e1t\u00e1it (a GPS koordin\u00e1t\u00e1kat vessz\u0151vel kell elv\u00e1lasztani). Megadhat egy entit\u00e1sazonos\u00edt\u00f3t is, amely ezeket az inform\u00e1ci\u00f3kat \u00e1llapot\u00e1ban megadja, egy entit\u00e1sazonos\u00edt\u00f3t sz\u00e9less\u00e9gi \u00e9s hossz\u00fas\u00e1gi attrib\u00fatumokkal vagy z\u00f3nabar\u00e1t nevet." } } }, @@ -23,9 +25,13 @@ "avoid_ferries": "Ker\u00fclje kompokat?", "avoid_subscription_roads": "Ker\u00fclje el az utakat, amelyekre matrica / el\u0151fizet\u00e9s sz\u00fcks\u00e9ges?", "avoid_toll_roads": "Ker\u00fclje a fizet\u0151s utakat?", + "excl_filter": "A kiv\u00e1lasztott \u00fatvonal le\u00edr\u00e1s\u00e1ban NEM szerepl\u0151 r\u00e9szl\u00e1nc", + "incl_filter": "R\u00e9szl\u00e1nc a kiv\u00e1lasztott \u00fatvonal le\u00edr\u00e1s\u00e1ban", "realtime": "Val\u00f3s idej\u0171 utaz\u00e1si id\u0151?", + "units": "Egys\u00e9gek", "vehicle_type": "J\u00e1rm\u0171 t\u00edpus" - } + }, + "description": "Az \"alfej\" bemenetek lehet\u0151v\u00e9 teszik az integr\u00e1ci\u00f3 k\u00e9nyszer\u00edt\u00e9s\u00e9t egy adott \u00fatvonal haszn\u00e1lat\u00e1ra, vagy az adott \u00fatvonal elker\u00fcl\u00e9s\u00e9re az id\u0151utaz\u00e1s kisz\u00e1m\u00edt\u00e1sakor." } } }, diff --git a/homeassistant/components/wemo/translations/hu.json b/homeassistant/components/wemo/translations/hu.json index ab799e90c74..bcb2f438353 100644 --- a/homeassistant/components/wemo/translations/hu.json +++ b/homeassistant/components/wemo/translations/hu.json @@ -4,5 +4,10 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." } + }, + "device_automation": { + "trigger_type": { + "long_press": "A Wemo gombot 2 m\u00e1sodpercig nyomva tartott\u00e1k." + } } } \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json index 4ddb08bb975..3b769a88b8f 100644 --- a/homeassistant/components/wilight/translations/hu.json +++ b/homeassistant/components/wilight/translations/hu.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_supported_device": "Ez a WiLight jelenleg nem t\u00e1mogatott", + "not_wilight_device": "Ez az eszk\u00f6z nem WiLight eszk\u00f6z" }, "flow_title": "{name}", "step": { "confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a WiLight {name} ? \n\n T\u00e1mogatja: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index 0d2c85e477d..769573bfc89 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -20,5 +20,14 @@ "title": "Felfedezett WLED eszk\u00f6z" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Tartsa a f\u0151f\u00e9nyt, m\u00e9g 1 LED szegmenssel is." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json index 2d8cdda9315..b393660f35a 100644 --- a/homeassistant/components/wolflink/translations/sensor.hu.json +++ b/homeassistant/components/wolflink/translations/sensor.hu.json @@ -1,6 +1,8 @@ { "state": { "wolflink__state": { + "auto_off_cool": "AutomataKiH\u0171t\u00e9s", + "automatik_aus": "Automatikus kikapcsol\u00e1s", "permanent": "\u00c1lland\u00f3" } } diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index d8bf9dfd866..1747b51c61a 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -3,14 +3,37 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva \u00e1llom\u00e1s vagy token.", + "not_xiaomi_miio": "Az eszk\u00f6zt (m\u00e9g) nem t\u00e1mogatja a Xiaomi Miio integr\u00e1ci\u00f3.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet." + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", + "cloud_login_error": "Nem siker\u00fclt bejelentkezni a Xioami Miio Cloud szolg\u00e1ltat\u00e1sba, ellen\u0151rizze a hiteles\u00edt\u0151 adatokat.", + "cloud_no_devices": "Nincs eszk\u00f6z ebben a Xiaomi Miio felh\u0151fi\u00f3kban.", + "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet.", + "unknown_device": "Az eszk\u00f6z modell nem ismert, nem tudja be\u00e1ll\u00edtani az eszk\u00f6zt a konfigur\u00e1ci\u00f3s folyamat seg\u00edts\u00e9g\u00e9vel." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Felh\u0151kiszolg\u00e1l\u00f3 orsz\u00e1ga", + "cloud_password": "Felh\u0151 jelszava", + "cloud_username": "Felh\u0151 felhaszn\u00e1l\u00f3neve", + "manual": "Konfigur\u00e1l\u00e1s manu\u00e1lisan (nem aj\u00e1nlott)" + }, + "description": "Jelentkezzen be a Xiaomi Miio felh\u0151be, a felh\u0151szerver haszn\u00e1lat\u00e1hoz l\u00e1sd: https://www.openhab.org/addons/bindings/miio/#country-servers.", + "title": "Csatlakozzon egy Xiaomi Miio eszk\u00f6zh\u00f6z vagy a Xiaomi Gateway-hez" + }, + "connect": { + "data": { + "model": "Eszk\u00f6z modell" + }, + "description": "V\u00e1lassza ki manu\u00e1lisan a modellt a t\u00e1mogatott modellek k\u00f6z\u00fcl.", + "title": "Csatlakozzon egy Xiaomi Miio eszk\u00f6zh\u00f6z vagy a Xiaomi Gateway-hez" + }, "device": { "data": { "host": "IP c\u00edm", @@ -34,15 +57,20 @@ "data": { "host": "IP c\u00edm", "token": "API Token" - } + }, + "description": "Sz\u00fcks\u00e9ge lesz a 32 karakteres API Token -re. Az utas\u00edt\u00e1sok\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Felh\u00edvjuk figyelm\u00e9t, hogy ez a API Token elt\u00e9r a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", + "title": "Csatlakozzon egy Xiaomi Miio eszk\u00f6zh\u00f6z vagy a Xiaomi Gateway-hez" }, "reauth_confirm": { + "description": "A tokenek friss\u00edt\u00e9s\u00e9hez vagy hi\u00e1nyz\u00f3 felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hozz\u00e1ad\u00e1s\u00e1hoz a Xiaomi Miio integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "select": { "data": { "select_device": "Miio eszk\u00f6z" - } + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtand\u00f3 Xiaomi Miio eszk\u00f6zt.", + "title": "Csatlakoz\u00e1s Xiaomi Miio eszk\u00f6zh\u00f6z vagy Xiaomi Gateway-hez" }, "user": { "data": { @@ -54,8 +82,15 @@ } }, "options": { + "error": { + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" + }, "step": { "init": { + "data": { + "cloud_subdevices": "Haszn\u00e1lja a felh\u0151t a csatlakoztatott alegys\u00e9gek megszerz\u00e9s\u00e9hez" + }, + "description": "Adja meg az opcion\u00e1lis be\u00e1ll\u00edt\u00e1sokat", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json index 0f973ce6dcc..9ddf75ca732 100644 --- a/homeassistant/components/yamaha_musiccast/translations/hu.json +++ b/homeassistant/components/yamaha_musiccast/translations/hu.json @@ -1,8 +1,13 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "yxc_control_url_missing": "A vez\u00e9rl\u0151 URL nincs megadva az ssdp le\u00edr\u00e1sban." }, + "error": { + "no_musiccast_device": "\u00dagy t\u0171nik, hogy ez az eszk\u00f6z nem MusicCast eszk\u00f6z." + }, + "flow_title": "MusicCast: {name}", "step": { "confirm": { "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" @@ -10,7 +15,8 @@ "user": { "data": { "host": "Hoszt" - } + }, + "description": "\u00c1ll\u00edtsa be a MusicCast-ot a Homeassistanttal val\u00f3 integr\u00e1ci\u00f3hoz." } } } diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json index cdb0839dd5a..26dc6cb5ba1 100644 --- a/homeassistant/components/yeelight/translations/hu.json +++ b/homeassistant/components/yeelight/translations/hu.json @@ -9,6 +9,9 @@ }, "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {model} ( {host} ) szolg\u00e1ltat\u00e1st?" + }, "pick_device": { "data": { "device": "Eszk\u00f6z" diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 896d4fbad30..2b078092ed7 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -19,7 +19,25 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Az \u00e9les\u00edt\u00e9si m\u0171veletekhez sz\u00fcks\u00e9ges k\u00f3d", + "alarm_failed_tries": "A riaszt\u00e1st kiv\u00e1lt\u00f3 egym\u00e1st k\u00f6vet\u0151 sikertelen k\u00f3dbejegyz\u00e9sek sz\u00e1ma", + "alarm_master_code": "A riaszt\u00f3 k\u00f6zpont(ok) mesterk\u00f3dja", + "title": "Riaszt\u00f3 vez\u00e9rl\u0151panel opci\u00f3k" + }, + "zha_options": { + "consider_unavailable_battery": "Fontolja meg, hogy az akkumul\u00e1toros eszk\u00f6z\u00f6k (m\u00e1sodpercek m\u00falva) nem \u00e9rhet\u0151k el", + "consider_unavailable_mains": "Fontolja meg, hogy a h\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k (m\u00e1sodpercek m\u00falva) nem \u00e9rhet\u0151k el", + "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", + "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s enged\u00e9lyez\u00e9se, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", + "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" + } + }, "device_automation": { + "trigger_subtype": { + "turn_off": "Kikapcsol\u00e1s" + }, "trigger_type": { "device_offline": "Eszk\u00f6z offline" } diff --git a/homeassistant/components/zoneminder/translations/hu.json b/homeassistant/components/zoneminder/translations/hu.json index a40d9299251..a449464e27f 100644 --- a/homeassistant/components/zoneminder/translations/hu.json +++ b/homeassistant/components/zoneminder/translations/hu.json @@ -10,18 +10,24 @@ "default": "ZoneMinder szerver hozz\u00e1adva." }, "error": { + "auth_fail": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, + "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "Host \u00e9s Port (pl. 10.10.0.4:8010)", "password": "Jelsz\u00f3", + "path": "ZM \u00fatvonal", + "path_zms": "ZMS el\u00e9r\u00e9si \u00fat", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "title": "Adja hozz\u00e1 a ZoneMinder szervert." } } } diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 484dbfa1824..74a8b9db316 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -1,24 +1,35 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Nem siker\u00fclt megszerezni a Z-Wave JS kieg\u00e9sz\u00edt\u0151 felfedez\u00e9si inform\u00e1ci\u00f3kat.", + "addon_info_failed": "Nem siker\u00fclt megszerezni a Z-Wave JS kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3it.", + "addon_install_failed": "Nem siker\u00fclt telep\u00edteni a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", + "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani a Z-Wave JS konfigur\u00e1ci\u00f3t.", "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { + "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_ws_url": "\u00c9rv\u00e9nytelen websocket URL", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { + "install_addon": "V\u00e1rjon, am\u00edg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", "start_addon": "V\u00e1rj am\u00edg a Z-Wave JS b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "configure_addon": { "data": { + "network_key": "H\u00e1l\u00f3zati kulcs", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - } + }, + "title": "Adja meg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 konfigur\u00e1ci\u00f3j\u00e1t" + }, + "hassio_confirm": { + "title": "\u00c1ll\u00edtsa be a Z-Wave JS integr\u00e1ci\u00f3t a Z-Wave JS kieg\u00e9sz\u00edt\u0151vel" }, "install_addon": { "title": "Elkezd\u0151d\u00f6tt a Z-Wave JS b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se" @@ -40,16 +51,68 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Konfigur\u00e1lja a(z) {subtype} param\u00e9ter \u00e9rt\u00e9k\u00e9t", + "node_status": "Csom\u00f3pont \u00e1llapota", + "value": "A Z-Wave \u00e9rt\u00e9k aktu\u00e1lis \u00e9rt\u00e9ke" + }, + "trigger_type": { + "event.notification.entry_control": "Bel\u00e9p\u00e9s-ellen\u0151rz\u00e9si \u00e9rtes\u00edt\u00e9st k\u00fcld\u00f6tt", + "event.notification.notification": "\u00c9rtes\u00edt\u00e9s elk\u00fcldve", + "event.value_notification.basic": "Alapvet\u0151 CC esem\u00e9ny a(z) {subtype}", + "event.value_notification.central_scene": "K\u00f6zponti jelenet m\u0171velet {subtype}", + "event.value_notification.scene_activation": "Jelenetaktiv\u00e1l\u00e1s {subtype}", + "state.node_status": "A csom\u00f3pont \u00e1llapota megv\u00e1ltozott" + } + }, "options": { + "abort": { + "addon_get_discovery_info_failed": "Nem siker\u00fclt megszerezni a Z-Wave JS kieg\u00e9sz\u00edt\u0151 felfedez\u00e9si inform\u00e1ci\u00f3kat.", + "addon_info_failed": "Nem siker\u00fclt megszerezni a Z-Wave JS kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3it.", + "addon_install_failed": "Nem siker\u00fclt telep\u00edteni a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", + "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani a Z-Wave JS konfigur\u00e1ci\u00f3t.", + "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rj\u00fck, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_ws_url": "\u00c9rv\u00e9nytelen websocket URL", + "unknown": "V\u00e1ratlan hiba" + }, + "progress": { + "install_addon": "V\u00e1rjon, am\u00edg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "V\u00e1rjon, am\u00edg a Z-Wave JS b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + }, "step": { "configure_addon": { "data": { + "emulate_hardware": "Hardver emul\u00e1ci\u00f3", "log_level": "Napl\u00f3szint", - "network_key": "H\u00e1l\u00f3zati kulcs" + "network_key": "H\u00e1l\u00f3zati kulcs", + "usb_path": "USB eszk\u00f6z \u00fatvonala" + }, + "title": "Adja meg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 konfigur\u00e1ci\u00f3j\u00e1t" + }, + "install_addon": { + "title": "Elkezd\u0151d\u00f6tt a Z-Wave JS kieg\u00e9sz\u00edt\u0151 telep\u00edt\u00e9se" + }, + "manual": { + "data": { + "url": "URL" } }, "on_supervisor": { + "data": { + "use_addon": "Haszn\u00e1lja a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt" + }, + "description": "Szeretn\u00e9 haszn\u00e1lni a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?", "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" + }, + "start_addon": { + "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." } } }, From 6b97a5ba8e1c196413dd7564fe007b58bb386b39 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 18 Jul 2021 20:17:36 -0400 Subject: [PATCH 352/818] Fix hisense_aehw4a1 test exclusion (#53084) * Fix hisense_aehw4a1 test coverage * add back __init__ * remove from hassfest Co-authored-by: Chris Talkington --- .coveragerc | 3 ++- script/hassfest/coverage.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 7c8954a5f80..030694df91f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -416,7 +416,8 @@ omit = homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py - homeassistant/components/hisense_aehw4a1/* + homeassistant/components/hisense_aehw4a1/__init__.py + homeassistant/components/hisense_aehw4a1/climate.py homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/__init__.py homeassistant/components/hive/climate.py diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index dd4b807d5c0..1a8609bb4e8 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -30,7 +30,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("fibaro", "scene.py"), ("hangouts", "config_flow.py"), ("harmony", "config_flow.py"), - ("hisense_aehw4a1", "config_flow.py"), ("huawei_lte", "config_flow.py"), ("ifttt", "config_flow.py"), ("ios", "config_flow.py"), From eeb01e638aba962a459ad244b61e200d473b6400 Mon Sep 17 00:00:00 2001 From: Brett Date: Mon, 19 Jul 2021 11:03:46 +1000 Subject: [PATCH 353/818] Add _attr_state_class (#52815) --- homeassistant/components/advantage_air/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 8c6834ac76e..edf079e1cba 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -1,7 +1,7 @@ """Sensor platform for Advantage Air integration.""" import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import PERCENTAGE from homeassistant.helpers import config_validation as cv, entity_platform @@ -84,6 +84,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" _attr_unit_of_measurement = PERCENTAGE + _attr_state_class = STATE_CLASS_MEASUREMENT @property def name(self): @@ -114,6 +115,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" _attr_unit_of_measurement = PERCENTAGE + _attr_state_class = STATE_CLASS_MEASUREMENT @property def name(self): From f24576b08d7e4e324aac4ee3b2ad395ad465e106 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 17:55:56 -1000 Subject: [PATCH 354/818] Show the name of the domain in HomeKit when selecting to include (#53169) --- .../components/homekit/config_flow.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 506b4f5c7a8..907fdcf79bf 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -1,4 +1,7 @@ """Config flow for HomeKit integration.""" +from __future__ import annotations + +import asyncio import random import re import string @@ -18,7 +21,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, ) -from homeassistant.core import callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( CONF_EXCLUDE_DOMAINS, @@ -26,6 +29,7 @@ from homeassistant.helpers.entityfilter import ( CONF_INCLUDE_DOMAINS, CONF_INCLUDE_ENTITIES, ) +from homeassistant.loader import async_get_integration from .const import ( CONF_AUTO_START, @@ -108,6 +112,21 @@ _EMPTY_ENTITY_FILTER = { } +async def _async_name_to_type_map(hass: HomeAssistant) -> dict[str, str]: + """Create a mapping of types of devices/entities HomeKit can support.""" + integrations = await asyncio.gather( + *[async_get_integration(hass, domain) for domain in SUPPORTED_DOMAINS], + return_exceptions=True, + ) + name_to_type_map = { + domain: domain + if isinstance(integrations[idx], Exception) + else integrations[idx].name + for idx, domain in enumerate(SUPPORTED_DOMAINS) + } + return name_to_type_map + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for HomeKit.""" @@ -127,13 +146,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS + name_to_type_map = await _async_name_to_type_map(self.hass) return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( CONF_INCLUDE_DOMAINS, default=default_domains - ): cv.multi_select(SUPPORTED_DOMAINS), + ): cv.multi_select(name_to_type_map), } ), ) @@ -437,6 +457,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): include_entities = entity_filter.get(CONF_INCLUDE_ENTITIES) if include_entities: domains.extend(_domains_set_from_entities(include_entities)) + name_to_type_map = await _async_name_to_type_map(self.hass) return self.async_show_form( step_id="init", @@ -448,7 +469,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Required( CONF_DOMAINS, default=domains, - ): cv.multi_select(SUPPORTED_DOMAINS), + ): cv.multi_select(name_to_type_map), } ), ) From 235f4476e857ce1625900254f39f0be4a56214f1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 08:29:26 +0200 Subject: [PATCH 355/818] Please mypy. (#53142) --- homeassistant/components/alarmdecoder/const.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/alarmdecoder/const.py b/homeassistant/components/alarmdecoder/const.py index f1bfb66f0d4..4aba16a9cf8 100644 --- a/homeassistant/components/alarmdecoder/const.py +++ b/homeassistant/components/alarmdecoder/const.py @@ -32,7 +32,7 @@ DEFAULT_ARM_OPTIONS = { CONF_AUTO_BYPASS: DEFAULT_AUTO_BYPASS, CONF_CODE_ARM_REQUIRED: DEFAULT_CODE_ARM_REQUIRED, } -DEFAULT_ZONE_OPTIONS = {} +DEFAULT_ZONE_OPTIONS: dict = {} DOMAIN = "alarmdecoder" diff --git a/mypy.ini b/mypy.ini index 1479bb7c700..0478be42acb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1062,9 +1062,6 @@ ignore_errors = true [mypy-homeassistant.components.aemet.*] ignore_errors = true -[mypy-homeassistant.components.alarmdecoder.*] -ignore_errors = true - [mypy-homeassistant.components.alexa.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 013e52c0327..f0334aae8d6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,7 +16,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.adguard.*", "homeassistant.components.aemet.*", - "homeassistant.components.alarmdecoder.*", "homeassistant.components.alexa.*", "homeassistant.components.almond.*", "homeassistant.components.amcrest.*", From bf831267cf4f7ed4679051f99deee4f98c831924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 22:22:12 -1000 Subject: [PATCH 356/818] Bump zeroconf to 0.33.0 (#53174) - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.32.1...0.33.0 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 199275623dc..9aba04a55cd 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.32.1"], + "requirements": ["zeroconf==0.33.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 908cd379886..0446cc3002c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.32.1 +zeroconf==0.33.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 9998e58687c..7aecb79615b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2419,7 +2419,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.32.1 +zeroconf==0.33.0 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6466fa8f5c7..7ad76c35abc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1319,7 +1319,7 @@ yeelight==0.6.3 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.32.1 +zeroconf==0.33.0 # homeassistant.components.zha zha-quirks==0.0.59 From c96f01df1fb3e52922de5416601d55ab9d63ca3f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 19 Jul 2021 10:32:21 +0200 Subject: [PATCH 357/818] Fix groups reporting incorrect supported color modes (#53088) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_light.py | 13 ++++++++++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index fbd420e4b16..0ae9e8c98b0 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==81" + "pydeconz==82" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 7aecb79615b..2f132aeae4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1370,7 +1370,7 @@ pydaikin==2.4.4 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==81 +pydeconz==82 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ad76c35abc..24177215993 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -765,7 +765,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.4.4 # homeassistant.components.deconz -pydeconz==81 +pydeconz==82 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 42dc04fc7ae..c2b12651fc0 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -742,7 +742,18 @@ async def test_groups(hass, aioclient_mock, input, expected): "name": "Group", "type": "LightGroup", "state": {"all_on": False, "any_on": True}, - "action": {}, + "action": { + "alert": "none", + "bri": 127, + "colormode": "hs", + "ct": 0, + "effect": "none", + "hue": 0, + "on": True, + "sat": 127, + "scene": None, + "xy": [0, 0], + }, "scenes": [], "lights": input["lights"], }, From 470f2dd73f9f476cf5e7ba58885af2aea43169d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 19 Jul 2021 11:46:09 +0300 Subject: [PATCH 358/818] Upgrade pyupgrade to 2.21.2, apply its changes (#52987) --- .pre-commit-config.yaml | 2 +- homeassistant/components/analytics/analytics.py | 8 ++++---- homeassistant/components/apple_tv/__init__.py | 4 ++-- homeassistant/components/arcam_fmj/__init__.py | 2 +- homeassistant/components/august/__init__.py | 4 ++-- homeassistant/components/august/activity.py | 4 ++-- homeassistant/components/automation/config.py | 4 ++-- homeassistant/components/awair/__init__.py | 2 +- homeassistant/components/axis/device.py | 4 ++-- homeassistant/components/climacell/__init__.py | 6 +++--- homeassistant/components/cloud/client.py | 2 +- .../components/devolo_home_control/__init__.py | 4 ++-- .../components/google_assistant/helpers.py | 4 ++-- .../components/google_assistant/smart_home.py | 4 ++-- .../components/home_plus_control/__init__.py | 4 ++-- .../components/homeassistant/__init__.py | 4 ++-- .../components/homekit_controller/__init__.py | 4 ++-- homeassistant/components/hyperion/__init__.py | 8 ++++---- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/kodi/browse_media.py | 6 +++--- homeassistant/components/logbook/__init__.py | 4 ++-- .../components/lovelace/system_health.py | 4 ++-- homeassistant/components/motioneye/__init__.py | 4 ++-- homeassistant/components/mysensors/__init__.py | 4 ++-- homeassistant/components/nilu/air_quality.py | 2 +- homeassistant/components/no_ip/__init__.py | 2 +- homeassistant/components/onewire/__init__.py | 4 ++-- homeassistant/components/ozw/__init__.py | 4 ++-- homeassistant/components/ping/device_tracker.py | 10 +++++----- homeassistant/components/radarr/sensor.py | 2 +- homeassistant/components/recorder/migration.py | 2 +- homeassistant/components/risco/__init__.py | 4 ++-- homeassistant/components/script/config.py | 4 ++-- .../components/sharkiq/update_coordinator.py | 2 +- homeassistant/components/sonarr/sensor.py | 2 +- homeassistant/components/sonos/__init__.py | 6 +++--- homeassistant/components/sonos/speaker.py | 2 +- homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/system_bridge/sensor.py | 4 ++-- homeassistant/components/tasmota/__init__.py | 4 ++-- homeassistant/components/vizio/config_flow.py | 4 ++-- homeassistant/components/vizio/media_player.py | 8 ++++---- .../components/websocket_api/commands.py | 2 +- homeassistant/components/wemo/__init__.py | 4 ++-- homeassistant/components/wemo/binary_sensor.py | 4 ++-- homeassistant/components/wemo/fan.py | 4 ++-- homeassistant/components/wemo/light.py | 4 ++-- homeassistant/components/wemo/switch.py | 4 ++-- homeassistant/components/withings/common.py | 16 ++++++---------- .../components/yamaha_musiccast/media_player.py | 6 ++---- homeassistant/components/zha/__init__.py | 4 ++-- homeassistant/components/zha/core/gateway.py | 8 ++++---- homeassistant/components/zha/light.py | 2 +- homeassistant/components/zwave_js/__init__.py | 4 ++-- homeassistant/components/zwave_js/services.py | 2 +- homeassistant/config_entries.py | 8 ++++---- homeassistant/helpers/collection.py | 12 ++++++------ homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/script.py | 8 ++++---- homeassistant/helpers/translation.py | 6 +++--- homeassistant/requirements.py | 4 ++-- homeassistant/setup.py | 4 ++-- requirements_test_pre_commit.txt | 2 +- tests/components/arcam_fmj/test_media_player.py | 4 ++-- tests/components/config/test_device_registry.py | 2 +- .../google_assistant/test_google_assistant.py | 4 ++-- .../components/google_assistant/test_helpers.py | 6 ++---- tests/components/hue/test_device_trigger.py | 4 ++-- tests/components/onboarding/test_views.py | 2 +- tests/components/withings/common.py | 8 +++----- tests/components/zha/test_discover.py | 2 +- tests/test_core.py | 4 ++-- tests/util/test_async.py | 2 +- 73 files changed, 152 insertions(+), 162 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31d5e9dd16c..952df031661 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.16.0 + rev: v2.21.2 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 571ffd90f22..42630ad2df1 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -171,10 +171,10 @@ class Analytics: ATTR_STATISTICS, False ): configured_integrations = await asyncio.gather( - *[ + *( async_get_integration(self.hass, domain) for domain in async_get_loaded_integrations(self.hass) - ], + ), return_exceptions=True, ) @@ -201,10 +201,10 @@ class Analytics: if supervisor_info is not None: installed_addons = await asyncio.gather( - *[ + *( hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG]) for addon in supervisor_info[ATTR_ADDONS] - ] + ) ) for addon in installed_addons: addons.append( diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 9a96f62dbd1..c4efa4ca09a 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -57,10 +57,10 @@ async def async_setup_entry(hass, entry): async def setup_platforms(): """Set up platforms and initiate connection.""" await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) await manager.init() diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 905e31c798b..c1df4fc0587 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -43,7 +43,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def _stop(_): asyncio.gather( - *[_await_cancel(task) for task in hass.data[DOMAIN_DATA_TASKS].values()] + *(_await_cancel(task) for task in hass.data[DOMAIN_DATA_TASKS].values()) ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 30374dcb220..f48068498c6 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -173,10 +173,10 @@ class AugustData(AugustSubscriberMixin): async def _async_refresh_device_detail_by_ids(self, device_ids_list): await asyncio.gather( - *[ + *( self._async_refresh_device_detail_by_id(device_id) for device_id in device_ids_list - ] + ) ) async def _async_refresh_device_detail_by_id(self, device_id): diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 77630b92511..f64b27c7c85 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -98,10 +98,10 @@ class ActivityStream(AugustSubscriberMixin): async def _async_update_device_activities(self, time): _LOGGER.debug("Start retrieving device activities") await asyncio.gather( - *[ + *( self._update_debounce[house_id].async_call() for house_id in self._house_ids - ] + ) ) self._last_update_time = time diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index e28fa5c477f..83076778b91 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -75,10 +75,10 @@ async def async_validate_config_item(hass, config, full_config=None): if CONF_CONDITION in config: config[CONF_CONDITION] = await asyncio.gather( - *[ + *( async_validate_condition_config(hass, cond) for cond in config[CONF_CONDITION] - ] + ) ) config[CONF_ACTION] = await script.async_validate_actions_config( diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 6af2850ea31..8199c3881c9 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -64,7 +64,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): user = await self._awair.user() devices = await user.devices() results = await gather( - *[self._fetch_air_data(device) for device in devices] + *(self._fetch_air_data(device) for device in devices) ) return {result.device.uuid: result for result in results} except AuthError as err: diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index e4987c77139..90eacf47965 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -226,12 +226,12 @@ class AxisNetworkDevice: async def start_platforms(): await asyncio.gather( - *[ + *( self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform ) for platform in PLATFORMS - ] + ) ) if self.option_events: self.api.stream.connection_status_callback.append( diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 09909ae4e3a..db006b6fb68 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -223,10 +223,10 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_PRECIPITATION_TYPE, - *[ + *( sensor_type[ATTR_FIELD] for sensor_type in CC_V3_SENSOR_TYPES - ], + ), ] ) data[FORECASTS][HOURLY] = await self._api.forecast_hourly( @@ -283,7 +283,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_ATTR_WIND_GUST, CC_ATTR_CLOUD_COVER, CC_ATTR_PRECIPITATION_TYPE, - *[sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES], + *(sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES), ], [ CC_ATTR_TEMPERATURE_LOW, diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c29e79f4e84..93c6fcd9086 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -148,7 +148,7 @@ class CloudClient(Interface): tasks.append(enable_google) if tasks: - await asyncio.gather(*[task(None) for task in tasks]) + await asyncio.gather(*(task(None) for task in tasks)) async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 297d3ac09c9..46b8f9dcaea 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -82,10 +82,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await asyncio.gather( - *[ + *( hass.async_add_executor_job(gateway.websocket_disconnect) for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] - ] + ) ) hass.data[DOMAIN][entry.entry_id]["listener"]() hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 885e79994ff..ebbed89347e 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -212,10 +212,10 @@ class AbstractConfig(ABC): async def async_sync_entities_all(self): """Sync all entities to Google for all registered agents.""" res = await gather( - *[ + *( self.async_sync_entities(agent_user_id) for agent_user_id in self._store.agent_user_ids - ] + ) ) return max(res, default=204) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 2ec51561eeb..dc55509b534 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -213,10 +213,10 @@ async def handle_devices_execute(hass, data, payload): executions[entity_id] = [execution] execute_results = await asyncio.gather( - *[ + *( _entity_execute(entities[entity_id], data, execution) for entity_id, execution in executions.items() - ] + ) ) for entity_id, result in zip(executions, execute_results): diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 718900533aa..e775b9d97aa 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -140,10 +140,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def start_platforms(): """Continue setting up the platforms.""" await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) # Only refresh the coordinator after all platforms are loaded. await coordinator.async_refresh() diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 44f0843871c..e798fda209b 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -248,10 +248,10 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C9 if not reload_entries: raise ValueError("There were no matching config entries to reload") await asyncio.gather( - *[ + *( hass.config_entries.async_reload(config_entry_id) for config_entry_id in reload_entries - ] + ) ) hass.helpers.service.async_register_admin_service( diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index f7507d09837..404b2f54ab0 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -228,10 +228,10 @@ async def async_setup(hass, config): async def _async_stop_homekit_controller(event): await asyncio.gather( - *[ + *( connection.async_unload() for connection in hass.data[KNOWN_DEVICES].values() - ] + ) ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 891f48e8738..36185c68758 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -273,10 +273,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def setup_then_listen() -> None: await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) assert hyperion_client if hyperion_client.instances is not None: @@ -306,12 +306,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # Disconnect the shared instance clients. await asyncio.gather( - *[ + *( config_data[CONF_INSTANCE_CLIENTS][ instance_num ].async_client_disconnect() for instance_num in config_data[CONF_INSTANCE_CLIENTS] - ] + ) ) # Disconnect the root client. diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index b56331bc80c..e6c1562f37e 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -245,7 +245,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await knx_module.xknx.stop() await asyncio.gather( - *[platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)] + *(platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)) ) await async_setup(hass, config) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index a7e87a6ae27..c36f05fc0db 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -71,7 +71,7 @@ async def build_item_response(media_library, payload, get_thumbnail_url=None): return None children = await asyncio.gather( - *[item_payload(item, get_thumbnail_url) for item in media] + *(item_payload(item, get_thumbnail_url) for item in media) ) if search_type in (MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE) and search_id == "": @@ -209,7 +209,7 @@ async def library_payload(): } library_info.children = await asyncio.gather( - *[ + *( item_payload( { "label": item["label"], @@ -220,7 +220,7 @@ async def library_payload(): for item in [ {"label": name, "type": type_} for type_, name in library.items() ] - ] + ) ) return library_info diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 87b66c57730..0a367907464 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -574,10 +574,10 @@ def _apply_event_types_filter(hass, query, event_types): def _apply_event_entity_id_matchers(events_query, entity_ids): return events_query.filter( sqlalchemy.or_( - *[ + *( Events.event_data.contains(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) for entity_id in entity_ids - ] + ) ) ) diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py index 2f4cfc6af76..29b53251f21 100644 --- a/homeassistant/components/lovelace/system_health.py +++ b/homeassistant/components/lovelace/system_health.py @@ -22,10 +22,10 @@ async def system_health_info(hass): health_info.update(await hass.data[DOMAIN]["resources"].async_get_info()) dashboards_info = await asyncio.gather( - *[ + *( hass.data[DOMAIN]["dashboards"][dashboard].async_get_info() for dashboard in hass.data[DOMAIN]["dashboards"] - ] + ) ) modes = set() diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 4fb3b2a19c6..d29f28e1704 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -352,10 +352,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def setup_then_listen() -> None: await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) entry.async_on_unload( coordinator.async_add_listener(_async_process_motioneye_cameras) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 2a958cee060..3d0f219c2a8 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -231,10 +231,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def finish() -> None: await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ] + ) ) await finish_setup(hass, entry, gateway) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index fb5b4c75798..77eb7945ab1 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -252,7 +252,7 @@ class NiluSensor(AirQualityEntity): sensors = self._api.data.sensors.values() if sensors: - max_index = max([s.pollution_index for s in sensors]) + max_index = max(s.pollution_index for s in sensors) self._max_aqi = max_index self._attrs[ATTR_POLLUTION_INDEX] = POLLUTION_INDEX[self._max_aqi] diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 2e9f5c77fbf..97015eab38a 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -60,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: password = config[DOMAIN].get(CONF_PASSWORD) timeout = config[DOMAIN].get(CONF_TIMEOUT) - auth_str = base64.b64encode(f"{user}:{password}".encode("utf-8")) + auth_str = base64.b64encode(f"{user}:{password}".encode()) session = hass.helpers.aiohttp_client.async_get_clientsession() diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 5ba813ce368..b99f095de7b 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -53,10 +53,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Start platforms and cleanup devices.""" # wait until all required platforms are ready await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) await cleanup_registry() diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 6d9b474977d..fc84a8ac7b0 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -264,10 +264,10 @@ async def async_setup_entry( # noqa: C901 async def start_platforms(): await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) if entry.data.get(CONF_USE_ADDON): mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager)) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index b5acecf9314..9fc149b5a1f 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -102,14 +102,14 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Update all the hosts on every interval time.""" results = await gather_with_concurrency( CONCURRENT_PING_LIMIT, - *[hass.async_add_executor_job(host.update) for host in hosts], + *(hass.async_add_executor_job(host.update) for host in hosts), ) await asyncio.gather( - *[ + *( async_see(dev_id=host.dev_id, source_type=SOURCE_TYPE_ROUTER) for idx, host in enumerate(hosts) if results[idx] - ] + ) ) else: @@ -124,11 +124,11 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): ) _LOGGER.debug("Multiping responses: %s", responses) await asyncio.gather( - *[ + *( async_see(dev_id=dev_id, source_type=SOURCE_TYPE_ROUTER) for idx, dev_id in enumerate(ip_to_dev_id.values()) if responses[idx].is_alive - ] + ) ) async def _async_update_interval(now): diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index fda7a37756b..add2580ee87 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -207,7 +207,7 @@ class RadarrSensor(SensorEntity): filter(lambda x: x["path"] in self.included, res.json()) ) self._state = "{:.2f}".format( - to_unit(sum([data["freeSpace"] for data in self.data]), self._unit) + to_unit(sum(data["freeSpace"] for data in self.data), self._unit) ) elif self.type == "status": self.data = res.json() diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 2ed676bfdb9..06391f2864d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -304,7 +304,7 @@ def _update_states_table_with_foreign_key_options(connection, engine): states_key_constraints = Base.metadata.tables[TABLE_STATES].foreign_key_constraints old_states_table = Table( # noqa: F841 pylint: disable=unused-variable - TABLE_STATES, MetaData(), *[alter["old_fk"] for alter in alters] + TABLE_STATES, MetaData(), *(alter["old_fk"] for alter in alters) ) for alter in alters: diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 4b33873e88d..ce43ce09988 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -57,10 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def start_platforms(): await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) await events_coordinator.async_refresh() diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index b1ebe88dc91..6993b7181e1 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -71,10 +71,10 @@ async def async_validate_config_item(hass, config, full_config=None): config = SCRIPT_ENTITY_SCHEMA(config) config[CONF_SEQUENCE] = await asyncio.gather( - *[ + *( async_validate_action_config(hass, action) for action in config[CONF_SEQUENCE] - ] + ) ) return config diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 01490c39297..16ed0e14d9a 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -69,7 +69,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): _LOGGER.debug("Updating sharkiq data") online_vacs = (self.shark_vacs[dsn] for dsn in self.online_dsns) - await asyncio.gather(*[self._async_update_vacuum(v) for v in online_vacs]) + await asyncio.gather(*(self._async_update_vacuum(v) for v in online_vacs)) except ( SharkIqAuthError, SharkIqNotAuthedError, diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index f1413aca52f..d173d42eaf7 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -162,7 +162,7 @@ class SonarrDiskspaceSensor(SonarrSensor): """Update entity.""" app = await self.sonarr.update() self._disks = app.disks - self._total_free = sum([disk.free for disk in self._disks]) + self._total_free = sum(disk.free for disk in self._disks) @property def extra_state_attributes(self) -> dict[str, Any] | None: diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 73e9ab0caf0..cf4592ff4b9 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -172,7 +172,7 @@ class SonosDiscoveryManager: async def _async_stop_event_listener(self, event: Event) -> None: await asyncio.gather( - *[speaker.async_unsubscribe() for speaker in self.data.discovered.values()], + *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()), return_exceptions=True, ) if events_asyncio.event_listener: @@ -285,10 +285,10 @@ class SonosDiscoveryManager: async def setup_platforms_and_discovery(self): """Set up platforms and discovery.""" await asyncio.gather( - *[ + *( self.hass.config_entries.async_forward_entry_setup(self.entry, platform) for platform in PLATFORMS - ] + ) ) self.entry.async_on_unload( self.hass.bus.async_listen_once( diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 19f65f963c3..7522f72b20b 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -352,7 +352,7 @@ class SonosSpeaker: """Cancel all subscriptions.""" _LOGGER.debug("Unsubscribing from events for %s", self.zone_name) await asyncio.gather( - *[subscription.unsubscribe() for subscription in self._subscriptions], + *(subscription.unsubscribe() for subscription in self._subscriptions), return_exceptions=True, ) self._subscriptions = [] diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 9896ec4177e..f0ba8b7dcea 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -257,7 +257,7 @@ class Scanner: EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start ) await asyncio.gather( - *[listener.async_start() for listener in self._ssdp_listeners] + *(listener.async_start() for listener in self._ssdp_listeners) ) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 68a3fbdbd39..d73d85fca1f 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -48,10 +48,10 @@ async def async_setup_entry( BridgeCpuSpeedSensor(coordinator, bridge), BridgeCpuTemperatureSensor(coordinator, bridge), BridgeCpuVoltageSensor(coordinator, bridge), - *[ + *( BridgeFilesystemSensor(coordinator, bridge, key) for key, _ in bridge.filesystem.fsSize.items() - ], + ), BridgeMemoryFreeSensor(coordinator, bridge), BridgeMemoryUsedSensor(coordinator, bridge), BridgeMemoryUsedPercentageSensor(coordinator, bridge), diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 27cfed82652..5599f887f8f 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -96,10 +96,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def start_platforms() -> None: await device_automation.async_setup_entry(hass, entry) await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS - ] + ) ) discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index c1aba99d84b..97d54f2e874 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -159,10 +159,10 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): ): cv.multi_select( [ APP_HOME["name"], - *[ + *( app["name"] for app in self.hass.data[DOMAIN][CONF_APPS].data - ], + ), ] ), } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index f5071ce146a..0cb2884a8b8 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -381,17 +381,17 @@ class VizioDevice(MediaPlayerEntity): # show the combination with , otherwise just return inputs if self._available_apps: return [ - *[ + *( _input for _input in self._available_inputs if _input not in INPUT_APPS - ], + ), *self._available_apps, - *[ + *( app for app in self._get_additional_app_names() if app not in self._available_apps - ], + ), ] return self._available_inputs diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 179fbcd1a30..fa8286084b6 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -268,7 +268,7 @@ async def handle_manifest_list( """Handle integrations command.""" loaded_integrations = async_get_loaded_integrations(hass) integrations = await asyncio.gather( - *[async_get_integration(hass, domain) for domain in loaded_integrations] + *(async_get_integration(hass, domain) for domain in loaded_integrations) ) connection.send_result( msg["id"], [integration.manifest for integration in integrations] diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9e9aa5ee278..2e803bc07bf 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -235,12 +235,12 @@ class WemoDiscovery: _LOGGER.debug("Adding statically configured WeMo devices") for device in await gather_with_concurrency( MAX_CONCURRENCY, - *[ + *( self._hass.async_add_executor_job( validate_static_config, host, port ) for host, port in self._static_config - ], + ), ): if device: await self._wemo_dispatcher.async_add_unique_device( diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 94d5a587c17..f3ba5e0ec52 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -21,10 +21,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo) await asyncio.gather( - *[ + *( _discovered_wemo(device) for device in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor") - ] + ) ) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 6910a4c8536..1582a0110cd 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -75,10 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) await asyncio.gather( - *[ + *( _discovered_wemo(device) for device in hass.data[WEMO_DOMAIN]["pending"].pop("fan") - ] + ) ) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 79f2e9b7172..0767c6b6603 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -50,10 +50,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo) await asyncio.gather( - *[ + *( _discovered_wemo(device) for device in hass.data[WEMO_DOMAIN]["pending"].pop("light") - ] + ) ) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 5e97031786c..a7031d669a4 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -40,10 +40,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo) await asyncio.gather( - *[ + *( _discovered_wemo(device) for device in hass.data[WEMO_DOMAIN]["pending"].pop("switch") - ] + ) ) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index a187786c995..646243e309d 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -673,21 +673,17 @@ class DataManager: response = await self._hass.async_add_executor_job(self._api.notify_list) subscribed_applis = frozenset( - [ - profile.appli - for profile in response.profiles - if profile.callbackurl == self._webhook_config.url - ] + profile.appli + for profile in response.profiles + if profile.callbackurl == self._webhook_config.url ) # Determine what subscriptions need to be created. ignored_applis = frozenset({NotifyAppli.USER, NotifyAppli.UNKNOWN}) to_add_applis = frozenset( - [ - appli - for appli in NotifyAppli - if appli not in subscribed_applis and appli not in ignored_applis - ] + appli + for appli in NotifyAppli + if appli not in subscribed_applis and appli not in ignored_applis ) # Subscribe to each one. diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index b67aa834008..c3269df6ca5 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -509,10 +509,8 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): def get_distribution_num(self) -> int: """Return the distribution_num (number of clients in the whole musiccast system).""" return sum( - [ - len(server.coordinator.data.group_client_list) - for server in self.get_all_server_entities() - ] + len(server.coordinator.data.group_client_list) + for server in self.get_all_server_entities() ) def is_part_of_group(self, group_server) -> bool: diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 801dedae0b6..e5b8c0936fd 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -145,10 +145,10 @@ async def async_unload_entry(hass, config_entry): # our components don't have unload methods so no need to look at return values await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_unload(config_entry, platform) for platform in PLATFORMS - ] + ) ) hass.data[DATA_ZHA][DATA_ZHA_SHUTDOWN_TASK]() diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 491f1a29774..d093c02d568 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -217,20 +217,20 @@ class ZHAGateway: _LOGGER.debug("Loading battery powered devices") await asyncio.gather( - *[ + *( _throttle(dev, cached=True) for dev in self.devices.values() if not dev.is_mains_powered - ] + ) ) _LOGGER.debug("Loading mains powered devices") await asyncio.gather( - *[ + *( _throttle(dev, cached=False) for dev in self.devices.values() if dev.is_mains_powered - ] + ) ) def device_joined(self, device): diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index c7001611aa0..ca84a579d14 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -419,7 +419,7 @@ class Light(BaseLight, ZhaEntity): self.async_accept_signal( self._level_channel, SIGNAL_SET_LEVEL, self.set_level ) - refresh_interval = random.randint(*[x * 60 for x in self._REFRESH_INTERVAL]) + refresh_interval = random.randint(*(x * 60 for x in self._REFRESH_INTERVAL)) self._cancel_refresh_handle = async_track_time_interval( self.hass, self._refresh, timedelta(seconds=refresh_interval) ) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 502510539cf..6cd0104e298 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -426,10 +426,10 @@ async def async_setup_entry( # noqa: C901 # run discovery on all ready nodes await asyncio.gather( - *[ + *( async_on_node_added(node) for node in client.driver.controller.nodes.values() - ] + ) ) # listen for new nodes being added to the mesh diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 1204f458bff..00f991433f4 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -447,4 +447,4 @@ class ZWaveServices: async def async_ping(self, service: ServiceCall) -> None: """Ping node(s).""" nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] - await asyncio.gather(*[node.async_ping() for node in nodes]) + await asyncio.gather(*(node.async_ping() for node in nodes)) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2bb8c4f3e29..ec074f81b95 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -850,7 +850,7 @@ class ConfigEntries: async def _async_shutdown(self, event: Event) -> None: """Call when Home Assistant is stopping.""" await asyncio.gather( - *[entry.async_shutdown() for entry in self._entries.values()] + *(entry.async_shutdown() for entry in self._entries.values()) ) await self.flow.async_shutdown() @@ -1082,10 +1082,10 @@ class ConfigEntries: """Forward the unloading of an entry to platforms.""" return all( await asyncio.gather( - *[ + *( self.async_forward_entry_unload(entry, platform) for platform in platforms - ] + ) ) ) @@ -1506,7 +1506,7 @@ class EntityRegistryDisabledHandler: ) await asyncio.gather( - *[self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload] + *(self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload) ) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 6d9815e54d5..f1ea4800c16 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -139,15 +139,15 @@ class ObservableCollection(ABC): async def notify_changes(self, change_sets: Iterable[CollectionChangeSet]) -> None: """Notify listeners of a change.""" await asyncio.gather( - *[ + *( listener(change_set.change_type, change_set.item_id, change_set.item) for listener in self.listeners for change_set in change_sets - ], - *[ + ), + *( change_set_listener(change_sets) for change_set_listener in self.change_set_listeners - ], + ), ) @@ -368,10 +368,10 @@ def sync_entity_lifecycle( new_entities = [ entity for entity in await asyncio.gather( - *[ + *( _func_map[change_set.change_type](change_set) for change_set in grouped - ] + ) ) if entity is not None ] diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 37c0a7620ab..7f329d02133 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -331,5 +331,5 @@ class EntityComponent: async def _async_shutdown(self, event: Event) -> None: """Call when Home Assistant is stopping.""" await asyncio.gather( - *[platform.async_shutdown() for platform in chain(self._platforms.values())] + *(platform.async_shutdown() for platform in chain(self._platforms.values())) ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 156ceb8e612..6108b4b8f65 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -239,7 +239,7 @@ async def async_validate_actions_config( ) -> list[ConfigType]: """Validate a list of actions.""" return await asyncio.gather( - *[async_validate_action_config(hass, action) for action in actions] + *(async_validate_action_config(hass, action) for action in actions) ) @@ -880,10 +880,10 @@ async def _async_stop_scripts_after_shutdown(hass, point_in_time): names = ", ".join([script["instance"].name for script in running_scripts]) _LOGGER.warning("Stopping scripts running too long after shutdown: %s", names) await asyncio.gather( - *[ + *( script["instance"].async_stop(update_state=False) for script in running_scripts - ] + ) ) @@ -902,7 +902,7 @@ async def _async_stop_scripts_at_shutdown(hass, event): names = ", ".join([script["instance"].name for script in running_scripts]) _LOGGER.debug("Stopping scripts running at shutdown: %s", names) await asyncio.gather( - *[script["instance"].async_stop() for script in running_scripts] + *(script["instance"].async_stop() for script in running_scripts) ) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index ed9049a8a13..a77cf3c2227 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -155,7 +155,7 @@ async def async_get_component_strings( domains, await gather_with_concurrency( MAX_LOAD_CONCURRENTLY, - *[async_get_integration(hass, domain) for domain in domains], + *(async_get_integration(hass, domain) for domain in domains), ), ) ) @@ -234,10 +234,10 @@ class _TranslationCache: # Fetch the English resources, as a fallback for missing keys languages = [LOCALE_EN] if language == LOCALE_EN else [LOCALE_EN, language] for translation_strings in await asyncio.gather( - *[ + *( async_get_component_strings(self.hass, lang, components) for lang in languages - ] + ) ): self._build_category_cache(language, components, translation_strings) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 02187fe8f0e..718ceba54ab 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -118,10 +118,10 @@ async def _async_process_integration( return results = await asyncio.gather( - *[ + *( async_get_integration_with_requirements(hass, dep, done) for dep in deps_to_check - ], + ), return_exceptions=True, ) for result in results: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 9b0c5282108..07bbaa22954 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -280,10 +280,10 @@ async def _async_setup_component( await hass.config_entries.flow.async_wait_init_flow_finish(domain) await asyncio.gather( - *[ + *( entry.async_setup(hass, integration=integration) for entry in hass.config_entries.async_entries(domain) - ] + ) ) hass.config.components.add(domain) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 3f473ae1592..589ecd666f8 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.16.0 +pyupgrade==2.21.2 yamllint==1.26.1 diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 05a070aada2..58609da6d6a 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -199,9 +199,9 @@ async def test_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): async def test_sound_mode_list(player, state): """Test sound mode list.""" player._get_2ch = Mock(return_value=True) # pylint: disable=W0212 - assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeMode2CH]) + assert sorted(player.sound_mode_list) == sorted(x.name for x in DecodeMode2CH) player._get_2ch = Mock(return_value=False) # pylint: disable=W0212 - assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeModeMCH]) + assert sorted(player.sound_mode_list) == sorted(x.name for x in DecodeModeMCH) async def test_sound_mode_zone_x(player, state): diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 04a353cb200..4e10413f14f 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -42,7 +42,7 @@ async def test_list_devices(hass, client, registry): await client.send_json({"id": 5, "type": "config/device_registry/list"}) msg = await client.receive_json() - dev1, dev2 = [entry.pop("id") for entry in msg["result"]] + dev1, dev2 = (entry.pop("id") for entry in msg["result"]) assert msg["result"] == [ { diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index bc9195264d9..2c3a61b8beb 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -134,8 +134,8 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] - assert sorted([dev["id"] for dev in devices]) == sorted( - [dev["id"] for dev in DEMO_DEVICES] + assert sorted(dev["id"] for dev in devices) == sorted( + dev["id"] for dev in DEMO_DEVICES ) for dev in devices: diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index d6094a771bd..e86156fa614 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -223,9 +223,7 @@ async def test_report_state_all(agents): data = {} with patch.object(config, "async_report_state") as mock: await config.async_report_state_all(data) - assert sorted(mock.mock_calls) == sorted( - [call(data, agent) for agent in agents] - ) + assert sorted(mock.mock_calls) == sorted(call(data, agent) for agent in agents) @pytest.mark.parametrize( @@ -241,7 +239,7 @@ async def test_sync_entities_all(agents, result): side_effect=lambda agent_user_id: agents[agent_user_id], ) as mock: res = await config.async_sync_entities_all() - assert sorted(mock.mock_calls) == sorted([call(agent) for agent in agents]) + assert sorted(mock.mock_calls) == sorted(call(agent) for agent in agents) assert res == result diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py index 5711c36da98..28bb989d475 100644 --- a/tests/components/hue/test_device_trigger.py +++ b/tests/components/hue/test_device_trigger.py @@ -74,7 +74,7 @@ async def test_get_triggers(hass, mock_bridge, device_reg): } expected_triggers = [ trigger_batt, - *[ + *( { "platform": "device", "domain": hue.DOMAIN, @@ -83,7 +83,7 @@ async def test_get_triggers(hass, mock_bridge, device_reg): "subtype": t_subtype, } for t_type, t_subtype in device_trigger.HUE_DIMMER_REMOTE.keys() - ], + ), ] assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index a921dfe39d4..75fcb9c0746 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -187,7 +187,7 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client): # Validate created areas area_registry = ar.async_get(hass) assert len(area_registry.areas) == 3 - assert sorted([area.name for area in area_registry.async_list_areas()]) == [ + assert sorted(area.name for area in area_registry.async_list_areas()) == [ "Bedroom", "Kitchen", "Living Room", diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index aea2d0152b2..9e6efeb37bf 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -292,11 +292,9 @@ def get_config_entries_for_user_id( ) -> tuple[ConfigEntry]: """Get a list of config entries that apply to a specific withings user.""" return tuple( - [ - config_entry - for config_entry in hass.config_entries.async_entries(const.DOMAIN) - if config_entry.data.get("token", {}).get("userid") == user_id - ] + config_entry + for config_entry in hass.config_entries.async_entries(const.DOMAIN) + if config_entry.data.get("token", {}).get("userid") == user_id ) diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index cd0e75a7237..9dc71d4aa25 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -266,7 +266,7 @@ async def test_discover_endpoint(device_info, channels_mock, hass): ) assert device_info["event_channels"] == sorted( - [ch.id for pool in channels.pools for ch in pool.client_channels.values()] + ch.id for pool in channels.pools for ch in pool.client_channels.values() ) assert new_ent.call_count == len( [ diff --git a/tests/test_core.py b/tests/test_core.py index 39c5b310537..5f17625a3de 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -315,9 +315,9 @@ def test_event_eq(): now = dt_util.utcnow() data = {"some": "attr"} context = ha.Context() - event1, event2 = [ + event1, event2 = ( ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2) - ] + ) assert event1 == event2 diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 19413c57aaa..cae47835cd8 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -165,7 +165,7 @@ async def test_gather_with_concurrency(): return runs results = await hasync.gather_with_concurrency( - 2, *[_increment_runs_if_in_time() for i in range(4)] + 2, *(_increment_runs_if_in_time() for i in range(4)) ) assert results == [2, 2, -1, -1] From 3cff15ae2fb29ea908836cfb2fb622c206d966b5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 19 Jul 2021 01:50:22 -0700 Subject: [PATCH 359/818] Bump google-nest-sdm to 0.3.0 (#53172) The primary update is to have additional static type checking with mypy --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 201ae40583e..dce39edbacf 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2f132aeae4b..c8009ef8d2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -697,7 +697,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.12 +google-nest-sdm==0.3.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24177215993..1077becb0c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.12 +google-nest-sdm==0.3.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From d12110556f25abe2dbbab9c94293f4f4da0fa299 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 19 Jul 2021 10:54:31 +0200 Subject: [PATCH 360/818] More restrictive state updates of UniFi uptime sensor (#53111) * More restrictive state updates of uptime sensor * Remove commented out old version of uptime test --- homeassistant/components/unifi/sensor.py | 28 ++++++++ tests/components/unifi/test_sensor.py | 91 +++++++++++++++--------- 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 8d34d3cabd7..338f695a2b4 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -133,6 +133,34 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, client, controller): + """Set up tracked client.""" + super().__init__(client, controller) + + self.last_updated_time = self.client.uptime + + @callback + def async_update_callback(self) -> None: + """Update sensor when time has changed significantly. + + This will help avoid unnecessary updates to the state machine. + """ + update_state = True + + if self.client.uptime < 1000000000: + if self.client.uptime > self.last_updated_time: + update_state = False + else: + if self.client.uptime <= self.last_updated_time: + update_state = False + + self.last_updated_time = self.client.uptime + + if not update_state: + return None + + super().async_update_callback() + @property def name(self) -> str: """Return the name of the client.""" diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index eec4fba7df9..fbf697e295f 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -4,6 +4,7 @@ from datetime import datetime from unittest.mock import patch from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED +import pytest from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -134,19 +135,29 @@ async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket): assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 -async def test_uptime_sensors(hass, aioclient_mock, mock_unifi_websocket): +@pytest.mark.parametrize( + "initial_uptime,event_uptime,new_uptime", + [ + # Uptime listed in epoch time should never change + (1609462800, 1609462800, 1612141200), + # Uptime counted in seconds increases with every event + (60, 64, 60), + ], +) +async def test_uptime_sensors( + hass, + aioclient_mock, + mock_unifi_websocket, + initial_uptime, + event_uptime, + new_uptime, +): """Verify that uptime sensors are working as expected.""" - client1 = { + uptime_client = { "mac": "00:00:00:00:00:01", "name": "client1", "oui": "Producer", - "uptime": 1609506061, - } - client2 = { - "hostname": "Client2", - "mac": "00:00:00:00:00:02", - "oui": "Producer", - "uptime": 60, + "uptime": initial_uptime, } options = { CONF_ALLOW_BANDWIDTH_SENSORS: False, @@ -155,32 +166,50 @@ async def test_uptime_sensors(hass, aioclient_mock, mock_unifi_websocket): CONF_TRACK_DEVICES: False, } - now = datetime(2021, 1, 1, 1, tzinfo=dt_util.UTC) + now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): config_entry = await setup_unifi_integration( hass, aioclient_mock, options=options, - clients_response=[client1, client2], + clients_response=[uptime_client], ) - assert len(hass.states.async_all()) == 3 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 - assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T13:01:01+00:00" - assert hass.states.get("sensor.client2_uptime").state == "2021-01-01T00:59:00+00:00" + assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 + assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" - # Verify state update + # Verify normal new event doesn't change uptime + # 4 seconds has passed - client1["uptime"] = 1609506062 - mock_unifi_websocket( - data={ - "meta": {"message": MESSAGE_CLIENT}, - "data": [client1], - } - ) - await hass.async_block_till_done() + uptime_client["uptime"] = event_uptime + now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_CLIENT}, + "data": [uptime_client], + } + ) + await hass.async_block_till_done() - assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T13:01:02+00:00" + assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + + # Verify new event change uptime + # 1 month has passed + + uptime_client["uptime"] = new_uptime + now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_CLIENT}, + "data": [uptime_client], + } + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.client1_uptime").state == "2021-02-01T01:00:00+00:00" # Disable option @@ -191,7 +220,6 @@ async def test_uptime_sensors(hass, aioclient_mock, mock_unifi_websocket): assert len(hass.states.async_all()) == 1 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 assert hass.states.get("sensor.client1_uptime") is None - assert hass.states.get("sensor.client2_uptime") is None # Enable option @@ -200,14 +228,13 @@ async def test_uptime_sensors(hass, aioclient_mock, mock_unifi_websocket): hass.config_entries.async_update_entry(config_entry, options=options.copy()) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 assert hass.states.get("sensor.client1_uptime") - assert hass.states.get("sensor.client2_uptime") # Try to add the sensors again, using a signal - clients_connected = {client1["mac"], client2["mac"]} + clients_connected = {uptime_client["mac"]} devices_connected = set() controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] @@ -220,8 +247,8 @@ async def test_uptime_sensors(hass, aioclient_mock, mock_unifi_websocket): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket): From bf0b19b05e6f46e19fc119690ac13c193a30fee8 Mon Sep 17 00:00:00 2001 From: Arto Jantunen Date: Mon, 19 Jul 2021 11:56:26 +0300 Subject: [PATCH 361/818] Add CO2 and efficiency sensors to Vallox (#48923) * Add Vallox efficiency sensor * Add Vallox CO2 sensor * Use the CO2 device class for the Vallox CO2 sensor --- homeassistant/components/vallox/sensor.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index b4269ac4451..ddfb9d1a7d3 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -5,6 +5,8 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, @@ -91,6 +93,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= unit_of_measurement=None, icon="mdi:filter", ), + ValloxSensor( + name=f"{name} Efficiency", + state_proxy=state_proxy, + metric_key="A_CYC_EXTRACT_EFFICIENCY", + device_class=None, + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + ), + ValloxSensor( + name=f"{name} CO2", + state_proxy=state_proxy, + metric_key="A_CYC_CO2_VALUE", + device_class=DEVICE_CLASS_CO2, + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + icon=None, + ), ] async_add_entities(sensors, update_before_add=False) From f51cb110d33afadd16b0c9b68e2248c6297f7ba3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 23:33:08 -1000 Subject: [PATCH 362/818] Run pyupgrade on homekit config_flow (#53180) - The original PR was run though the CI before the new pyupgrade --- homeassistant/components/homekit/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 907fdcf79bf..459bb050f65 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -115,7 +115,7 @@ _EMPTY_ENTITY_FILTER = { async def _async_name_to_type_map(hass: HomeAssistant) -> dict[str, str]: """Create a mapping of types of devices/entities HomeKit can support.""" integrations = await asyncio.gather( - *[async_get_integration(hass, domain) for domain in SUPPORTED_DOMAINS], + *(async_get_integration(hass, domain) for domain in SUPPORTED_DOMAINS), return_exceptions=True, ) name_to_type_map = { From 7b27725ec1f8daa4557f213184be8f395e6f9ea8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 19 Jul 2021 06:00:38 -0400 Subject: [PATCH 363/818] Cleanup redundant coveragerc entries (#53171) --- .coveragerc | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.coveragerc b/.coveragerc index 030694df91f..320bdc78c5d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -399,10 +399,6 @@ omit = homeassistant/components/habitica/const.py homeassistant/components/habitica/sensor.py homeassistant/components/hangouts/* - homeassistant/components/hangouts/__init__.py - homeassistant/components/hangouts/const.py - homeassistant/components/hangouts/hangouts_bot.py - homeassistant/components/hangouts/hangups_utils.py homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harmony/const.py homeassistant/components/harmony/data.py @@ -436,9 +432,6 @@ omit = homeassistant/components/home_connect/sensor.py homeassistant/components/home_connect/switch.py homeassistant/components/homematic/* - homeassistant/components/homematic/climate.py - homeassistant/components/homematic/cover.py - homeassistant/components/homematic/notify.py homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py homeassistant/components/homeworks/* @@ -532,8 +525,6 @@ omit = homeassistant/components/kira/* homeassistant/components/kiwi/lock.py homeassistant/components/knx/* - homeassistant/components/knx/climate.py - homeassistant/components/knx/cover.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/const.py @@ -663,7 +654,6 @@ omit = homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* homeassistant/components/mycroft/* - homeassistant/components/mycroft/notify.py homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/binary_sensor.py homeassistant/components/mysensors/climate.py @@ -835,8 +825,6 @@ omit = homeassistant/components/radarr/sensor.py homeassistant/components/radiotherm/climate.py homeassistant/components/rainbird/* - homeassistant/components/rainbird/sensor.py - homeassistant/components/rainbird/switch.py homeassistant/components/raincloud/* homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/binary_sensor.py @@ -883,7 +871,6 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* - homeassistant/components/rpi_gpio/cover.py homeassistant/components/rpi_gpio_pwm/light.py homeassistant/components/rpi_pfio/* homeassistant/components/rpi_rf/switch.py @@ -903,7 +890,6 @@ omit = homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py homeassistant/components/scsgate/* - homeassistant/components/scsgate/cover.py homeassistant/components/sendgrid/notify.py homeassistant/components/sense/* homeassistant/components/sensehat/light.py @@ -1028,7 +1014,6 @@ omit = homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* - homeassistant/components/tado/device_tracker.py homeassistant/components/tahoma/* homeassistant/components/tank_utility/sensor.py homeassistant/components/tankerkoenig/* @@ -1096,9 +1081,6 @@ omit = homeassistant/components/traccar/const.py homeassistant/components/trackr/device_tracker.py homeassistant/components/tradfri/* - homeassistant/components/tradfri/light.py - homeassistant/components/tradfri/cover.py - homeassistant/components/tradfri/base_class.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/sensor.py From 671e838085bdeb253d7aca8fc55677a774d935b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jul 2021 00:07:12 -1000 Subject: [PATCH 364/818] Execute scripts from HomeKit (#53106) --- .../components/homekit/type_switches.py | 17 +++++++---- .../components/homekit/test_type_switches.py | 30 +++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 8ea19897420..381110a4e79 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -55,6 +55,9 @@ VALVE_TYPE = { } +ACTIVATE_ONLY_SWITCH_DOMAINS = {"scene", "script"} + + @TYPES.register("Outlet") class Outlet(HomeAccessory): """Generate an Outlet accessory.""" @@ -98,7 +101,7 @@ class Switch(HomeAccessory): def __init__(self, *args): """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) - self._domain = split_entity_id(self.entity_id)[0] + self._domain, self._object_id = split_entity_id(self.entity_id) state = self.hass.states.get(self.entity_id) self.activate_only = self.is_activate(self.hass.states.get(self.entity_id)) @@ -113,9 +116,7 @@ class Switch(HomeAccessory): def is_activate(self, state): """Check if entity is activate only.""" - if self._domain == "scene": - return True - return False + return self._domain in ACTIVATE_ONLY_SWITCH_DOMAINS def reset_switch(self, *args): """Reset switch to emulate activate click.""" @@ -129,8 +130,14 @@ class Switch(HomeAccessory): if self.activate_only and not value: _LOGGER.debug("%s: Ignoring turn_off call", self.entity_id) return + params = {ATTR_ENTITY_ID: self.entity_id} - service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + if self._domain == "script": + service = self._object_id + params = {} + else: + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.async_call_service(self._domain, service, params) if self.activate_only: diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 2ce0acfc8bc..455f7a6141a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -84,7 +84,6 @@ async def test_outlet_set_state(hass, hk_driver, events): ("automation.test", {}), ("input_boolean.test", {}), ("remote.test", {}), - ("script.test", {}), ("switch.test", {}), ], ) @@ -340,8 +339,9 @@ async def test_reset_switch(hass, hk_driver, events): assert len(events) == 1 -async def test_reset_switch_reload(hass, hk_driver, events): - """Test reset switch after script reload.""" +async def test_script_switch(hass, hk_driver, events): + """Test if script switch accessory is reset correctly.""" + domain = "script" entity_id = "script.test" hass.states.async_set(entity_id, None) @@ -350,8 +350,28 @@ async def test_reset_switch_reload(hass, hk_driver, events): await acc.run() await hass.async_block_till_done() - assert acc.activate_only is False + assert acc.activate_only is True + assert acc.char_on.value is False - hass.states.async_set(entity_id, None) + call_turn_on = async_mock_service(hass, domain, "test") + call_turn_off = async_mock_service(hass, domain, "turn_off") + + await hass.async_add_executor_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert acc.char_on.value is True + assert call_turn_on + assert call_turn_on[0].data == {} + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 + assert not call_turn_off + + await hass.async_add_executor_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert acc.char_on.value is False + assert len(events) == 1 From 12b29e28958a89b9a383202f6e38b68ec7b8e132 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jul 2021 00:10:32 -1000 Subject: [PATCH 365/818] Bump zeroconf to 0.33.1 (#53179) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 9aba04a55cd..e41b30ea26c 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.33.0"], + "requirements": ["zeroconf==0.33.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0446cc3002c..f2448e7025b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.0 +zeroconf==0.33.1 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index c8009ef8d2b..3a591583911 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2419,7 +2419,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.0 +zeroconf==0.33.1 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1077becb0c7..fd91ae7b4ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1319,7 +1319,7 @@ yeelight==0.6.3 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.0 +zeroconf==0.33.1 # homeassistant.components.zha zha-quirks==0.0.59 From 78ef02f4d96ca29b279146170e5351e40a153243 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 13:01:50 +0200 Subject: [PATCH 366/818] Allow pymodbus to reconnect in running system (not startup) (#53020) Allow pymodbus to reconnect (not during startup). --- homeassistant/components/modbus/modbus.py | 13 +++-- tests/components/modbus/test_init.py | 67 ----------------------- 2 files changed, 8 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index f2b033bec3e..8d2ea46e293 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -249,17 +249,22 @@ class ModbusHub: for entry in self._pb_call.values(): entry[ENTRY_FUNC] = getattr(self._client, entry[ENTRY_NAME]) + await self.async_connect_task() + return True + + async def async_connect_task(self): + """Try to connect, and retry if needed.""" async with self._lock: if not await self.hass.async_add_executor_job(self._pymodbus_connect): - self._log_error("initial connect failed, no retry", error_state=False) - return False + err = f"{self._config_name} connect failed, retry in pymodbus" + self._log_error(err, error_state=False) + return # Start counting down to allow modbus requests. if self._config_delay: self._async_cancel_listener = async_call_later( self.hass, self._config_delay, self.async_end_delay ) - return True @callback def async_end_delay(self, args): @@ -313,8 +318,6 @@ class ModbusHub: return None if not self._client: return None - if not self._client.is_socket_open(): - return None async with self._lock: result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 7dda164e5eb..ec68b98efa0 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -515,35 +515,6 @@ async def test_pymodbus_constructor_fail(hass, caplog): assert mock_pb.called -@pytest.mark.parametrize( - "do_connect,do_exception,do_text", - [ - [False, None, "initial connect failed, no retry"], - [True, ModbusException("no connect"), "Modbus Error: no connect"], - ], -) -async def test_pymodbus_connect_fail( - hass, do_connect, do_exception, do_text, caplog, mock_pymodbus -): - """Run test for failing pymodbus connect.""" - config = { - DOMAIN: [ - { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, - } - ] - } - caplog.set_level(logging.ERROR) - mock_pymodbus.connect.return_value = do_connect - mock_pymodbus.connect.side_effect = do_exception - assert await async_setup_component(hass, DOMAIN, config) is False - await hass.async_block_till_done() - assert caplog.messages[0].startswith(f"Pymodbus: {do_text}") - assert caplog.records[0].levelname == "ERROR" - - async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): """Run test for failing pymodbus close.""" config = { @@ -563,44 +534,6 @@ async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): # Close() is called as part of teardown -async def test_disconnect(hass, mock_pymodbus): - """Run test for startup delay.""" - - # the purpose of this test is to test a device disconnect - # We "hijiack" a binary_sensor to make a proper blackbox test. - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" - config = { - DOMAIN: [ - { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, - CONF_NAME: TEST_MODBUS_NAME, - CONF_BINARY_SENSORS: [ - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_NAME: f"{TEST_SENSOR_NAME}", - CONF_ADDRESS: 52, - }, - ], - } - ] - } - mock_pymodbus.read_coils.return_value = ReadResult([0x01]) - mock_pymodbus.is_socket_open.return_value = False - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - - # pass first scan_interval - now = now + timedelta(seconds=20) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - async def test_delay(hass, mock_pymodbus): """Run test for startup delay.""" From 0802dd42933edb85d3b7317dca3646573708baab Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 13:05:13 +0200 Subject: [PATCH 367/818] Activate mypy for eafm (#53184) --- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 2 files changed, 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 0478be42acb..f05daa919bd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1143,9 +1143,6 @@ ignore_errors = true [mypy-homeassistant.components.dynalite.*] ignore_errors = true -[mypy-homeassistant.components.eafm.*] -ignore_errors = true - [mypy-homeassistant.components.edl21.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f0334aae8d6..31e1390f1d4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -43,7 +43,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", "homeassistant.components.dynalite.*", - "homeassistant.components.eafm.*", "homeassistant.components.edl21.*", "homeassistant.components.elkm1.*", "homeassistant.components.emonitor.*", From 51d16202abc6e8b446f85022d22803935c4274ca Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 14:14:09 +0200 Subject: [PATCH 368/818] Correct typing in control4 and activate mypy (#53156) * Correct typing and activate mypy. * Review comments.:wq --- homeassistant/components/control4/__init__.py | 10 ++++++---- homeassistant/components/control4/light.py | 8 +++++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 6e4af61e24b..b7806e665f3 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -1,4 +1,6 @@ """The Control4 integration.""" +from __future__ import annotations + import json import logging @@ -149,9 +151,9 @@ class Control4Entity(CoordinatorEntity): coordinator: DataUpdateCoordinator, name: str, idx: int, - device_name: str, - device_manufacturer: str, - device_model: str, + device_name: str | None, + device_manufacturer: str | None, + device_model: str | None, device_id: int, ) -> None: """Initialize a Control4 entity.""" @@ -174,7 +176,7 @@ class Control4Entity(CoordinatorEntity): @property def unique_id(self) -> str: """Return a unique ID.""" - return self._idx + return str(self._idx) @property def device_info(self): diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 46fc35398fe..38eca233f27 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -1,4 +1,6 @@ """Platform for Control4 Lights.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging @@ -145,9 +147,9 @@ class Control4Light(Control4Entity, LightEntity): coordinator: DataUpdateCoordinator, name: str, idx: int, - device_name: str, - device_manufacturer: str, - device_model: str, + device_name: str | None, + device_manufacturer: str | None, + device_model: str | None, device_id: int, is_dimmer: bool, ) -> None: diff --git a/mypy.ini b/mypy.ini index f05daa919bd..ac94b19c446 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1116,9 +1116,6 @@ ignore_errors = true [mypy-homeassistant.components.config.*] ignore_errors = true -[mypy-homeassistant.components.control4.*] -ignore_errors = true - [mypy-homeassistant.components.conversation.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 31e1390f1d4..88106d96c32 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -34,7 +34,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.*", "homeassistant.components.cloudflare.*", "homeassistant.components.config.*", - "homeassistant.components.control4.*", "homeassistant.components.conversation.*", "homeassistant.components.deconz.*", "homeassistant.components.demo.*", From ea6e32576259728760c20ca91a5ac90ce9081193 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 19 Jul 2021 16:28:40 +0300 Subject: [PATCH 369/818] Add Switcher config flow discovery support (#52316) --- .coveragerc | 2 - .strict-typing | 1 + .../components/switcher_kis/__init__.py | 198 ++++++++++---- .../components/switcher_kis/config_flow.py | 49 ++++ .../components/switcher_kis/const.py | 14 +- .../components/switcher_kis/manifest.json | 5 +- .../components/switcher_kis/sensor.py | 90 +++---- .../components/switcher_kis/services.yaml | 2 + .../components/switcher_kis/strings.json | 13 + .../components/switcher_kis/switch.py | 244 +++++++++-------- .../switcher_kis/translations/en.json | 13 + .../components/switcher_kis/utils.py | 54 ++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 14 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/mypy_config.py | 1 - tests/components/switcher_kis/__init__.py | 15 ++ tests/components/switcher_kis/conftest.py | 201 +++----------- tests/components/switcher_kis/consts.py | 64 +++-- .../switcher_kis/test_config_flow.py | 106 ++++++++ tests/components/switcher_kis/test_init.py | 255 ++++++------------ tests/components/switcher_kis/test_sensor.py | 93 +++++++ .../components/switcher_kis/test_services.py | 162 +++++++++++ tests/components/switcher_kis/test_switch.py | 127 +++++++++ 25 files changed, 1146 insertions(+), 582 deletions(-) create mode 100644 homeassistant/components/switcher_kis/config_flow.py create mode 100644 homeassistant/components/switcher_kis/strings.json create mode 100644 homeassistant/components/switcher_kis/translations/en.json create mode 100644 homeassistant/components/switcher_kis/utils.py create mode 100644 tests/components/switcher_kis/test_config_flow.py create mode 100644 tests/components/switcher_kis/test_sensor.py create mode 100644 tests/components/switcher_kis/test_services.py create mode 100644 tests/components/switcher_kis/test_switch.py diff --git a/.coveragerc b/.coveragerc index 320bdc78c5d..6d7363c3d04 100644 --- a/.coveragerc +++ b/.coveragerc @@ -992,8 +992,6 @@ omit = homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbot/switch.py - homeassistant/components/switcher_kis/sensor.py - homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/sensor.py diff --git a/.strict-typing b/.strict-typing index 1a12b1c5299..981e3872eb5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -79,6 +79,7 @@ homeassistant.components.ssdp.* homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.switch.* +homeassistant.components.switcher_kis.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* homeassistant.components.tag.* diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index ef196220656..6c13067cd7f 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,85 +1,181 @@ """The Switcher integration.""" from __future__ import annotations -from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for -from datetime import datetime, timedelta +from datetime import timedelta import logging -from aioswitcher.bridge import SwitcherV2Bridge +from aioswitcher.device import SwitcherBase import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + update_coordinator, +) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import EventType from .const import ( CONF_DEVICE_PASSWORD, CONF_PHONE_ID, DATA_DEVICE, + DATA_DISCOVERY, DOMAIN, - SIGNAL_SWITCHER_DEVICE_UPDATE, + MAX_UPDATE_INTERVAL_SEC, + SIGNAL_DEVICE_ADD, ) +from .utils import async_start_bridge, async_stop_bridge + +PLATFORMS = ["switch", "sensor"] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PHONE_ID): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_DEVICE_PASSWORD): cv.string, - } - ) - }, +CCONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PHONE_ID): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_DEVICE_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the switcher component.""" - phone_id = config[DOMAIN][CONF_PHONE_ID] - device_id = config[DOMAIN][CONF_DEVICE_ID] - device_password = config[DOMAIN][CONF_DEVICE_PASSWORD] + hass.data.setdefault(DOMAIN, {}) - v2bridge = SwitcherV2Bridge(hass.loop, phone_id, device_id, device_password) + if DOMAIN not in config: + return True - await v2bridge.start() + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + ) + return True - async def async_stop_bridge(event: EventType) -> None: - """On Home Assistant stop, gracefully stop the bridge if running.""" - await v2bridge.stop() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_bridge) - - try: - device_data = await wait_for(v2bridge.queue.get(), timeout=10.0) - except (Asyncio_TimeoutError, RuntimeError): - _LOGGER.exception("Failed to get response from device") - await v2bridge.stop() - return False - hass.data[DOMAIN] = {DATA_DEVICE: device_data} - - hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config)) - hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Switcher from a config entry.""" + hass.data[DOMAIN][DATA_DEVICE] = {} @callback - def device_updates(timestamp: datetime | None) -> None: - """Use for updating the device data from the queue.""" - if v2bridge.running: - try: - device_new_data = v2bridge.queue.get_nowait() - if device_new_data: - async_dispatcher_send( - hass, SIGNAL_SWITCHER_DEVICE_UPDATE, device_new_data - ) - except QueueEmpty: - pass + def on_device_data_callback(device: SwitcherBase) -> None: + """Use as a callback for device data.""" - async_track_time_interval(hass, device_updates, timedelta(seconds=4)) + # Existing device update device data + if device.device_id in hass.data[DOMAIN][DATA_DEVICE]: + wrapper: SwitcherDeviceWrapper = hass.data[DOMAIN][DATA_DEVICE][ + device.device_id + ] + wrapper.async_set_updated_data(device) + return + + # New device - create device + _LOGGER.info( + "Discovered Switcher device - id: %s, name: %s, type: %s (%s)", + device.device_id, + device.name, + device.device_type.value, + device.device_type.hex_rep, + ) + + wrapper = hass.data[DOMAIN][DATA_DEVICE][ + device.device_id + ] = SwitcherDeviceWrapper(hass, entry, device) + hass.async_create_task(wrapper.async_setup()) + + async def platforms_setup_task() -> None: + # Must be ready before dispatcher is called + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_setup(entry, platform) + + discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) + if discovery_task is not None: + discovered_devices = await discovery_task + for device in discovered_devices.values(): + on_device_data_callback(device) + + await async_start_bridge(hass, on_device_data_callback) + + hass.async_create_task(platforms_setup_task()) + + @callback + async def stop_bridge(event: Event) -> None: + await async_stop_bridge(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) return True + + +class SwitcherDeviceWrapper(update_coordinator.DataUpdateCoordinator): + """Wrapper for a Switcher device with Home Assistant specific functions.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: SwitcherBase + ) -> None: + """Initialize the Switcher device wrapper.""" + super().__init__( + hass, + _LOGGER, + name=device.name, + update_interval=timedelta(seconds=MAX_UPDATE_INTERVAL_SEC), + ) + self.hass = hass + self.entry = entry + self.data = device + + async def _async_update_data(self) -> None: + """Mark device offline if no data.""" + raise update_coordinator.UpdateFailed( + f"Device {self.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds" + ) + + @property + def model(self) -> str: + """Switcher device model.""" + return self.data.device_type.value # type: ignore[no-any-return] + + @property + def device_id(self) -> str: + """Switcher device id.""" + return self.data.device_id # type: ignore[no-any-return] + + @property + def mac_address(self) -> str: + """Switcher device mac address.""" + return self.data.mac_address # type: ignore[no-any-return] + + async def async_setup(self) -> None: + """Set up the wrapper.""" + dev_reg = await device_registry.async_get_registry(self.hass) + dev_reg.async_get_or_create( + config_entry_id=self.entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac_address)}, + identifiers={(DOMAIN, self.device_id)}, + manufacturer="Switcher", + name=self.name, + model=self.model, + ) + async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADD, self) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + await async_stop_bridge(hass) + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(DATA_DEVICE) + + return unload_ok diff --git a/homeassistant/components/switcher_kis/config_flow.py b/homeassistant/components/switcher_kis/config_flow.py new file mode 100644 index 00000000000..3c758715205 --- /dev/null +++ b/homeassistant/components/switcher_kis/config_flow.py @@ -0,0 +1,49 @@ +"""Config flow for Switcher integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_DISCOVERY, DOMAIN +from .utils import async_discover_devices + + +class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle Switcher config flow.""" + + async def async_step_import(self, import_config: ConfigType) -> FlowResult: + """Handle a flow initiated by import.""" + if self._async_current_entries(True): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Switcher", data={}) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the start of the config flow.""" + if self._async_current_entries(True): + return self.async_abort(reason="single_instance_allowed") + + self.hass.data.setdefault(DOMAIN, {}) + if DATA_DISCOVERY not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][DATA_DISCOVERY] = self.hass.async_create_task( + async_discover_devices() + ) + + return self.async_show_form(step_id="confirm") + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user-confirmation of the config flow.""" + discovered_devices = await self.hass.data[DOMAIN][DATA_DISCOVERY] + + if len(discovered_devices) == 0: + self.hass.data[DOMAIN].pop(DATA_DISCOVERY) + return self.async_abort(reason="no_devices_found") + + return self.async_create_entry(title="Switcher", data={}) diff --git a/homeassistant/components/switcher_kis/const.py b/homeassistant/components/switcher_kis/const.py index acd6c070337..88b6e447446 100644 --- a/homeassistant/components/switcher_kis/const.py +++ b/homeassistant/components/switcher_kis/const.py @@ -1,20 +1,22 @@ """Constants for the Switcher integration.""" - DOMAIN = "switcher_kis" CONF_DEVICE_PASSWORD = "device_password" CONF_PHONE_ID = "phone_id" +DATA_BRIDGE = "bridge" DATA_DEVICE = "device" +DATA_DISCOVERY = "discovery" -SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update" +DISCOVERY_TIME_SEC = 6 -ATTR_AUTO_OFF_SET = "auto_off_set" -ATTR_ELECTRIC_CURRENT = "electric_current" -ATTR_REMAINING_TIME = "remaining_time" +SIGNAL_DEVICE_ADD = "switcher_device_add" +# Services CONF_AUTO_OFF = "auto_off" CONF_TIMER_MINUTES = "timer_minutes" - SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" + +# Defines the maximum interval device must send an update before it marked unavailable +MAX_UPDATE_INTERVAL_SEC = 20 diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 84527954a2d..e982855e497 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,6 +3,7 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi","@thecode"], - "requirements": ["aioswitcher==1.2.3"], - "iot_class": "local_push" + "requirements": ["aioswitcher==2.0.4"], + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 2325d382b56..58a32e69154 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -3,8 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from aioswitcher.consts import WAITING_TEXT -from aioswitcher.devices import SwitcherV2Device +from aioswitcher.device import DeviceCategory from homeassistant.components.sensor import ( DEVICE_CLASS_CURRENT, @@ -12,13 +11,17 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE +from . import SwitcherDeviceWrapper +from .const import SIGNAL_DEVICE_ADD @dataclass @@ -31,7 +34,6 @@ class AttributeDescription: device_class: str | None = None state_class: str | None = None default_enabled: bool = True - default_value: float | int | str | None = None POWER_SENSORS = { @@ -40,14 +42,12 @@ POWER_SENSORS = { unit=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - default_value=0, ), "electric_current": AttributeDescription( name="Electric Current", unit=ELECTRICAL_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, - default_value=0.0, ), } @@ -55,77 +55,73 @@ TIME_SENSORS = { "remaining_time": AttributeDescription( name="Remaining Time", icon="mdi:av-timer", - default_value="00:00:00", ), "auto_off_set": AttributeDescription( name="Auto Shutdown", icon="mdi:progress-clock", default_enabled=False, - default_value="00:00:00", ), } -SENSORS = {**POWER_SENSORS, **TIME_SENSORS} +POWER_PLUG_SENSORS = POWER_SENSORS +WATER_HEATER_SENSORS = {**POWER_SENSORS, **TIME_SENSORS} -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: dict, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType, ) -> None: """Set up Switcher sensor from config entry.""" - device_data = hass.data[DOMAIN][DATA_DEVICE] - async_add_entities( - SwitcherSensorEntity(device_data, attribute, sensor) - for attribute, sensor in SENSORS.items() + @callback + def async_add_sensors(wrapper: SwitcherDeviceWrapper) -> None: + """Add sensors from Switcher device.""" + if wrapper.data.device_type.category == DeviceCategory.POWER_PLUG: + async_add_entities( + SwitcherSensorEntity(wrapper, attribute, info) + for attribute, info in POWER_PLUG_SENSORS.items() + ) + elif wrapper.data.device_type.category == DeviceCategory.WATER_HEATER: + async_add_entities( + SwitcherSensorEntity(wrapper, attribute, info) + for attribute, info in WATER_HEATER_SENSORS.items() + ) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_sensors) ) -class SwitcherSensorEntity(SensorEntity): +class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): """Representation of a Switcher sensor entity.""" def __init__( self, - device_data: SwitcherV2Device, + wrapper: SwitcherDeviceWrapper, attribute: str, description: AttributeDescription, ) -> None: """Initialize the entity.""" - self._device_data = device_data + super().__init__(wrapper) + self.wrapper = wrapper self.attribute = attribute - self.description = description # Entity class attributes - self._attr_name = f"{self._device_data.name} {self.description.name}" - self._attr_icon = self.description.icon - self._attr_unit_of_measurement = self.description.unit - self._attr_device_class = self.description.device_class - self._attr_entity_registry_enabled_default = self.description.default_enabled - self._attr_should_poll = False + self._attr_name = f"{wrapper.name} {description.name}" + self._attr_icon = description.icon + self._attr_unit_of_measurement = description.unit + self._attr_device_class = description.device_class + self._attr_entity_registry_enabled_default = description.default_enabled - self._attr_unique_id = f"{self._device_data.device_id}-{self._device_data.mac_addr}-{self.attribute}" + self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}-{attribute}" + self._attr_device_info = { + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address) + } + } @property def state(self) -> StateType: """Return value of sensor.""" - value = getattr(self._device_data, self.attribute) - if value and value is not WAITING_TEXT: - return value - - return self.description.default_value - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data - ) - ) - - @callback - def async_update_data(self, device_data: SwitcherV2Device) -> None: - """Update the entity data.""" - self._device_data = device_data - self.async_write_ha_state() + return getattr(self.wrapper.data, self.attribute) # type: ignore[no-any-return] diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index b4b2728fc2e..752b7f3de4c 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -5,6 +5,7 @@ set_auto_off: entity: integration: switcher_kis domain: switch + device_class: switch fields: auto_off: name: Auto off @@ -21,6 +22,7 @@ turn_on_with_timer: entity: integration: switcher_kis domain: switch + device_class: switch fields: timer_minutes: name: Timer diff --git a/homeassistant/components/switcher_kis/strings.json b/homeassistant/components/switcher_kis/strings.json new file mode 100644 index 00000000000..9f3518bcf8d --- /dev/null +++ b/homeassistant/components/switcher_kis/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 21ebcf54cc7..c36fd0c208e 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,33 +1,42 @@ -"""Home Assistant Switcher Component Switch platform.""" +"""Switcher integration Switch platform.""" from __future__ import annotations -from aioswitcher.api import SwitcherV2Api -from aioswitcher.api.messages import SwitcherV2ControlResponseMSG -from aioswitcher.consts import ( - COMMAND_OFF, - COMMAND_ON, - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, -) -from aioswitcher.devices import SwitcherV2Device +import asyncio +from datetime import timedelta +import logging +from typing import Any + +from aioswitcher.api import Command, SwitcherApi, SwitcherBaseResponse +from aioswitcher.device import DeviceCategory, DeviceState import voluptuous as vol -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + DEVICE_CLASS_SWITCH, + SwitchEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + entity_platform, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import SwitcherDeviceWrapper from .const import ( CONF_AUTO_OFF, CONF_TIMER_MINUTES, - DATA_DEVICE, - DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_TURN_ON_WITH_TIMER_NAME, - SIGNAL_SWITCHER_DEVICE_UPDATE, + SIGNAL_DEVICE_ADD, ) +_LOGGER = logging.getLogger(__name__) + SERVICE_SET_AUTO_OFF_SCHEMA = { vol.Required(CONF_AUTO_OFF): cv.time_period_str, } @@ -39,135 +48,142 @@ SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { } -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: dict, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: dict, ) -> None: - """Set up the switcher platform for the switch component.""" - if discovery_info is None: - return - - async def async_set_auto_off_service(entity, service_call: ServiceCall) -> None: - """Use for handling setting device auto-off service calls.""" - async with SwitcherV2Api( - hass.loop, - device_data.ip_addr, - device_data.phone_id, - device_data.device_id, - device_data.device_password, - ) as swapi: - await swapi.set_auto_shutdown(service_call.data[CONF_AUTO_OFF]) - - async def async_turn_on_with_timer_service( - entity, service_call: ServiceCall - ) -> None: - """Use for handling turning device on with a timer service calls.""" - async with SwitcherV2Api( - hass.loop, - device_data.ip_addr, - device_data.phone_id, - device_data.device_id, - device_data.device_password, - ) as swapi: - await swapi.control_device( - COMMAND_ON, service_call.data[CONF_TIMER_MINUTES] - ) - - device_data = hass.data[DOMAIN][DATA_DEVICE] - async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])]) - + """Set up Switcher switch from config entry.""" platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_SCHEMA, - async_set_auto_off_service, + "async_set_auto_off_service", ) platform.async_register_entity_service( SERVICE_TURN_ON_WITH_TIMER_NAME, SERVICE_TURN_ON_WITH_TIMER_SCHEMA, - async_turn_on_with_timer_service, + "async_turn_on_with_timer_service", + ) + + @callback + def async_add_switch(wrapper: SwitcherDeviceWrapper) -> None: + """Add switch from Switcher device.""" + if wrapper.data.device_type.category == DeviceCategory.POWER_PLUG: + async_add_entities([SwitcherPowerPlugSwitchEntity(wrapper)]) + elif wrapper.data.device_type.category == DeviceCategory.WATER_HEATER: + async_add_entities([SwitcherWaterHeaterSwitchEntity(wrapper)]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_switch) ) -class SwitcherControl(SwitchEntity): - """Home Assistant switch entity.""" +class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity): + """Representation of a Switcher switch entity.""" - def __init__(self, device_data: SwitcherV2Device) -> None: + def __init__(self, wrapper: SwitcherDeviceWrapper) -> None: """Initialize the entity.""" - self._self_initiated = False - self._device_data = device_data - self._state = device_data.state + super().__init__(wrapper) + self.wrapper = wrapper + self.control_result: bool | None = None - @property - def name(self) -> str: - """Return the device's name.""" - return self._device_data.name + # Entity class attributes + self._attr_name = wrapper.name + self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}" + self._attr_device_info = { + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address) + } + } - @property - def should_poll(self) -> bool: - """Return False, entity pushes its state to HA.""" - return False + @callback + def _handle_coordinator_update(self) -> None: + """When device updates, clear control result that overrides state.""" + self.control_result = None + self.async_write_ha_state() - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._device_data.device_id}-{self._device_data.mac_addr}" + async def _async_call_api(self, api: str, *args: Any) -> None: + """Call Switcher API.""" + _LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args) + response: SwitcherBaseResponse = None + error = None + + try: + async with SwitcherApi( + self.wrapper.data.ip_address, self.wrapper.device_id + ) as swapi: + response = await getattr(swapi, api)(*args) + except (asyncio.TimeoutError, OSError, RuntimeError) as err: + error = repr(err) + + if error or not response or not response.successful: + _LOGGER.error( + "Call api for %s failed, api: '%s', args: %s, response/error: %s", + self.name, + api, + args, + response or error, + ) + self.wrapper.last_update_success = False @property def is_on(self) -> bool: """Return True if entity is on.""" - return self._state == SWITCHER_STATE_ON + if self.control_result is not None: + return self.control_result - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF] - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data - ) - ) - - @callback - def async_update_data(self, device_data: SwitcherV2Device) -> None: - """Update the entity data.""" - if self._self_initiated: - self._self_initiated = False - else: - self._device_data = device_data - self._state = self._device_data.state - self.async_write_ha_state() + return bool(self.wrapper.data.device_state == DeviceState.ON) async def async_turn_on(self, **kwargs: dict) -> None: """Turn the entity on.""" - await self._control_device(True) + await self._async_call_api("control_device", Command.ON) + self.control_result = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: dict) -> None: """Turn the entity off.""" - await self._control_device(False) + await self._async_call_api("control_device", Command.OFF) + self.control_result = False + self.async_write_ha_state() - async def _control_device(self, send_on: bool) -> None: - """Turn the entity on or off.""" - response: SwitcherV2ControlResponseMSG = None - async with SwitcherV2Api( - self.hass.loop, - self._device_data.ip_addr, - self._device_data.phone_id, - self._device_data.device_id, - self._device_data.device_password, - ) as swapi: - response = await swapi.control_device( - COMMAND_ON if send_on else COMMAND_OFF - ) + async def async_set_auto_off_service(self, auto_off: timedelta) -> None: + """Use for handling setting device auto-off service calls.""" + _LOGGER.warning( + "Service '%s' is not supported by %s", + SERVICE_SET_AUTO_OFF_NAME, + self.name, + ) - if response and response.successful: - self._self_initiated = True - self._state = SWITCHER_STATE_ON if send_on else SWITCHER_STATE_OFF - self.async_write_ha_state() + async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None: + """Use for turning device on with a timer service calls.""" + _LOGGER.warning( + "Service '%s' is not supported by %s", + SERVICE_TURN_ON_WITH_TIMER_NAME, + self.name, + ) + + +class SwitcherPowerPlugSwitchEntity(SwitcherBaseSwitchEntity): + """Representation of a Switcher power plug switch entity.""" + + _attr_device_class = DEVICE_CLASS_OUTLET + + +class SwitcherWaterHeaterSwitchEntity(SwitcherBaseSwitchEntity): + """Representation of a Switcher water heater switch entity.""" + + _attr_device_class = DEVICE_CLASS_SWITCH + + async def async_set_auto_off_service(self, auto_off: timedelta) -> None: + """Use for handling setting device auto-off service calls.""" + await self._async_call_api("set_auto_shutdown", auto_off) + self.async_write_ha_state() + + async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None: + """Use for turning device on with a timer service calls.""" + await self._async_call_api("control_device", Command.ON, timer_minutes) + self.control_result = True + self.async_write_ha_state() diff --git a/homeassistant/components/switcher_kis/translations/en.json b/homeassistant/components/switcher_kis/translations/en.json new file mode 100644 index 00000000000..f05becffed3 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/utils.py b/homeassistant/components/switcher_kis/utils.py new file mode 100644 index 00000000000..b2cc45cf67c --- /dev/null +++ b/homeassistant/components/switcher_kis/utils.py @@ -0,0 +1,54 @@ +"""Switcher integration helpers functions.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Callable + +from aioswitcher.bridge import SwitcherBase, SwitcherBridge + +from homeassistant.core import HomeAssistant, callback + +from .const import DATA_BRIDGE, DISCOVERY_TIME_SEC, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_start_bridge( + hass: HomeAssistant, on_device_callback: Callable[[SwitcherBase], Any] +) -> None: + """Start switcher UDP bridge.""" + bridge = hass.data[DOMAIN][DATA_BRIDGE] = SwitcherBridge(on_device_callback) + _LOGGER.debug("Starting Switcher bridge") + await bridge.start() + + +async def async_stop_bridge(hass: HomeAssistant) -> None: + """Stop switcher UDP bridge.""" + bridge: SwitcherBridge = hass.data[DOMAIN].get(DATA_BRIDGE) + if bridge is not None: + _LOGGER.debug("Stopping Switcher bridge") + await bridge.stop() + hass.data[DOMAIN].pop(DATA_BRIDGE) + + +async def async_discover_devices() -> dict[str, SwitcherBase]: + """Discover Switcher devices.""" + _LOGGER.debug("Starting discovery") + discovered_devices = {} + + @callback + def on_device_data_callback(device: SwitcherBase) -> None: + """Use as a callback for device data.""" + if device.device_id in discovered_devices: + return + + discovered_devices[device.device_id] = device + + bridge = SwitcherBridge(on_device_data_callback) + await bridge.start() + await asyncio.sleep(DISCOVERY_TIME_SEC) + await bridge.stop() + + _LOGGER.debug("Finished discovery, discovered devices: %s", len(discovered_devices)) + return discovered_devices diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e71503ce5fc..b04d4f50dd8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -251,6 +251,7 @@ FLOWS = [ "srp_energy", "starline", "subaru", + "switcher_kis", "syncthing", "syncthru", "synology_dsm", diff --git a/mypy.ini b/mypy.ini index ac94b19c446..6245144f54f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -880,6 +880,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.switcher_kis.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.synology_dsm.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1539,9 +1550,6 @@ ignore_errors = true [mypy-homeassistant.components.switchbot.*] ignore_errors = true -[mypy-homeassistant.components.switcher_kis.*] -ignore_errors = true - [mypy-homeassistant.components.synology_srm.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index 3a591583911..516885a9bb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -236,7 +236,7 @@ aiorecollect==1.0.5 aioshelly==0.6.4 # homeassistant.components.switcher_kis -aioswitcher==1.2.3 +aioswitcher==2.0.4 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd91ae7b4ae..b1ffcb18d5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aiorecollect==1.0.5 aioshelly==0.6.4 # homeassistant.components.switcher_kis -aioswitcher==1.2.3 +aioswitcher==2.0.4 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 88106d96c32..f0f7abe7b7e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -175,7 +175,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.stt.*", "homeassistant.components.surepetcare.*", "homeassistant.components.switchbot.*", - "homeassistant.components.switcher_kis.*", "homeassistant.components.synology_srm.*", "homeassistant.components.system_health.*", "homeassistant.components.system_log.*", diff --git a/tests/components/switcher_kis/__init__.py b/tests/components/switcher_kis/__init__.py index 46fbe073ab0..671af5e11b9 100644 --- a/tests/components/switcher_kis/__init__.py +++ b/tests/components/switcher_kis/__init__.py @@ -1 +1,16 @@ """Test cases and object for the Switcher integration tests.""" +from homeassistant.components.switcher_kis.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Switcher integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index fda5f39922d..3578e3ac6c9 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -1,194 +1,61 @@ """Common fixtures and objects for the Switcher integration tests.""" -from __future__ import annotations +from unittest.mock import AsyncMock, Mock, patch -from asyncio import Queue -from datetime import datetime -from typing import Any, Generator -from unittest.mock import AsyncMock, patch - -from pytest import fixture - -from .consts import ( - DUMMY_AUTO_OFF_SET, - DUMMY_DEVICE_ID, - DUMMY_DEVICE_NAME, - DUMMY_DEVICE_PASSWORD, - DUMMY_DEVICE_STATE, - DUMMY_ELECTRIC_CURRENT, - DUMMY_IP_ADDRESS, - DUMMY_MAC_ADDRESS, - DUMMY_PHONE_ID, - DUMMY_POWER_CONSUMPTION, - DUMMY_REMAINING_TIME, -) +import pytest -@patch("aioswitcher.devices.SwitcherV2Device") -class MockSwitcherV2Device: - """Class for mocking the aioswitcher.devices.SwitcherV2Device object.""" +@pytest.fixture +def mock_bridge(request): + """Return a mocked SwitcherBridge.""" + with patch( + "homeassistant.components.switcher_kis.utils.SwitcherBridge", autospec=True + ) as bridge_mock: + bridge = bridge_mock.return_value - def __init__(self) -> None: - """Initialize the object.""" - self._last_state_change = datetime.now() + bridge.devices = [] + if hasattr(request, "param") and request.param: + bridge.devices = request.param - @property - def device_id(self) -> str: - """Return the device id.""" - return DUMMY_DEVICE_ID + async def start(): + bridge.is_running = True - @property - def ip_addr(self) -> str: - """Return the ip address.""" - return DUMMY_IP_ADDRESS + for device in bridge.devices: + bridge_mock.call_args[0][0](device) - @property - def mac_addr(self) -> str: - """Return the mac address.""" - return DUMMY_MAC_ADDRESS + def mock_callbacks(devices): + for device in devices: + bridge_mock.call_args[0][0](device) - @property - def name(self) -> str: - """Return the device name.""" - return DUMMY_DEVICE_NAME + async def stop(): + bridge.is_running = False - @property - def state(self) -> str: - """Return the device state.""" - return DUMMY_DEVICE_STATE + bridge.start = AsyncMock(side_effect=start) + bridge.mock_callbacks = Mock(side_effect=mock_callbacks) + bridge.stop = AsyncMock(side_effect=stop) - @property - def remaining_time(self) -> str | None: - """Return the time left to auto-off.""" - return DUMMY_REMAINING_TIME - - @property - def auto_off_set(self) -> str: - """Return the auto-off configuration value.""" - return DUMMY_AUTO_OFF_SET - - @property - def power_consumption(self) -> int: - """Return the power consumption in watts.""" - return DUMMY_POWER_CONSUMPTION - - @property - def electric_current(self) -> float: - """Return the power consumption in amps.""" - return DUMMY_ELECTRIC_CURRENT - - @property - def phone_id(self) -> str: - """Return the phone id.""" - return DUMMY_PHONE_ID - - @property - def device_password(self) -> str: - """Return the device password.""" - return DUMMY_DEVICE_PASSWORD - - @property - def last_data_update(self) -> datetime: - """Return the timestamp of the last update.""" - return datetime.now() - - @property - def last_state_change(self) -> datetime: - """Return the timestamp of the state change.""" - return self._last_state_change + yield bridge -@fixture(name="mock_bridge") -def mock_bridge_fixture() -> Generator[None, Any, None]: - """Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge.""" - queue = Queue() - - async def mock_queue(): - """Mock asyncio's Queue.""" - await queue.put(MockSwitcherV2Device()) - return await queue.get() - - mock_bridge = AsyncMock() +@pytest.fixture +def mock_api(): + """Fixture for mocking aioswitcher.api.SwitcherApi.""" + api_mock = AsyncMock() patchers = [ patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.start", - new=mock_bridge, + "homeassistant.components.switcher_kis.switch.SwitcherApi.connect", + new=api_mock, ), patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", - new=mock_bridge, - ), - patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.queue", - get=mock_queue, - ), - patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.running", - return_value=True, + "homeassistant.components.switcher_kis.switch.SwitcherApi.disconnect", + new=api_mock, ), ] for patcher in patchers: patcher.start() - yield - - for patcher in patchers: - patcher.stop() - - -@fixture(name="mock_failed_bridge") -def mock_failed_bridge_fixture() -> Generator[None, Any, None]: - """Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge.""" - - async def mock_queue(): - """Mock asyncio's Queue.""" - raise RuntimeError - - patchers = [ - patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.start", - return_value=None, - ), - patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", - return_value=None, - ), - patch( - "homeassistant.components.switcher_kis.SwitcherV2Bridge.queue", - get=mock_queue, - ), - ] - - for patcher in patchers: - patcher.start() - - yield - - for patcher in patchers: - patcher.stop() - - -@fixture(name="mock_api") -def mock_api_fixture() -> Generator[AsyncMock, Any, None]: - """Fixture for mocking aioswitcher.api.SwitcherV2Api.""" - mock_api = AsyncMock() - - patchers = [ - patch( - "homeassistant.components.switcher_kis.switch.SwitcherV2Api.connect", - new=mock_api, - ), - patch( - "homeassistant.components.switcher_kis.switch.SwitcherV2Api.disconnect", - new=mock_api, - ), - ] - - for patcher in patchers: - patcher.start() - - yield + yield api_mock for patcher in patchers: patcher.stop() diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index ab5951710f4..e200d92e026 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -1,5 +1,12 @@ """Constants for the Switcher integration tests.""" +from aioswitcher.device import ( + DeviceState, + DeviceType, + SwitcherPowerPlug, + SwitcherWaterHeater, +) + from homeassistant.components.switcher_kis import ( CONF_DEVICE_ID, CONF_DEVICE_PASSWORD, @@ -8,27 +15,54 @@ from homeassistant.components.switcher_kis import ( ) DUMMY_AUTO_OFF_SET = "01:30:00" -DUMMY_TIMER_MINUTES_SET = "90" -DUMMY_DEVICE_ID = "a123bc" -DUMMY_DEVICE_NAME = "Device Name" +DUMMY_AUTO_SHUT_DOWN = "02:00:00" +DUMMY_DEVICE_ID1 = "a123bc" +DUMMY_DEVICE_ID2 = "cafe12" +DUMMY_DEVICE_NAME1 = "Plug 23BC" +DUMMY_DEVICE_NAME2 = "Heater FE12" DUMMY_DEVICE_PASSWORD = "12345678" -DUMMY_DEVICE_STATE = "on" -DUMMY_ELECTRIC_CURRENT = 12.8 -DUMMY_ICON = "mdi:dummy-icon" -DUMMY_IP_ADDRESS = "192.168.100.157" -DUMMY_MAC_ADDRESS = "A1:B2:C3:45:67:D8" -DUMMY_NAME = "boiler" +DUMMY_ELECTRIC_CURRENT1 = 0.5 +DUMMY_ELECTRIC_CURRENT2 = 12.8 +DUMMY_IP_ADDRESS1 = "192.168.100.157" +DUMMY_IP_ADDRESS2 = "192.168.100.158" +DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8" +DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9" DUMMY_PHONE_ID = "1234" -DUMMY_POWER_CONSUMPTION = 2780 +DUMMY_POWER_CONSUMPTION1 = 100 +DUMMY_POWER_CONSUMPTION2 = 2780 DUMMY_REMAINING_TIME = "01:29:32" +DUMMY_TIMER_MINUTES_SET = "90" -# Adjust if any modification were made to DUMMY_DEVICE_NAME -SWITCH_ENTITY_ID = "switch.device_name" - -MANDATORY_CONFIGURATION = { +YAML_CONFIG = { DOMAIN: { CONF_PHONE_ID: DUMMY_PHONE_ID, - CONF_DEVICE_ID: DUMMY_DEVICE_ID, + CONF_DEVICE_ID: DUMMY_DEVICE_ID1, CONF_DEVICE_PASSWORD: DUMMY_DEVICE_PASSWORD, } } + +DUMMY_PLUG_DEVICE = SwitcherPowerPlug( + DeviceType.POWER_PLUG, + DeviceState.ON, + DUMMY_DEVICE_ID1, + DUMMY_IP_ADDRESS1, + DUMMY_MAC_ADDRESS1, + DUMMY_DEVICE_NAME1, + DUMMY_POWER_CONSUMPTION1, + DUMMY_ELECTRIC_CURRENT1, +) + +DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater( + DeviceType.V4, + DeviceState.ON, + DUMMY_DEVICE_ID2, + DUMMY_IP_ADDRESS2, + DUMMY_MAC_ADDRESS2, + DUMMY_DEVICE_NAME2, + DUMMY_POWER_CONSUMPTION2, + DUMMY_ELECTRIC_CURRENT2, + DUMMY_REMAINING_TIME, + DUMMY_AUTO_SHUT_DOWN, +) + +DUMMY_SWITCHER_DEVICES = [DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE] diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py new file mode 100644 index 00000000000..07a2396a0d9 --- /dev/null +++ b/tests/components/switcher_kis/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the Switcher config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.switcher_kis.const import DATA_DISCOVERY, DOMAIN +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE + +from tests.common import MockConfigEntry + + +async def test_import(hass): + """Test import step.""" + with patch( + "homeassistant.components.switcher_kis.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Switcher" + assert result["data"] == {} + + +@pytest.mark.parametrize( + "mock_bridge", + [ + [ + DUMMY_PLUG_DEVICE, + DUMMY_WATER_HEATER_DEVICE, + # Make sure we don't detect the same device twice + DUMMY_WATER_HEATER_DEVICE, + ] + ], + indirect=True, +) +async def test_user_setup(hass, mock_bridge): + """Test we can finish a config flow.""" + with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert mock_bridge.is_running is False + assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2 + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] is None + + with patch( + "homeassistant.components.switcher_kis.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Switcher" + assert result2["result"].data == {} + + +async def test_user_setup_abort_no_devices_found(hass, mock_bridge): + """Test we abort a config flow if no devices found.""" + with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert mock_bridge.is_running is False + assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0 + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "no_devices_found" + + +@pytest.mark.parametrize( + "source", + [ + config_entries.SOURCE_IMPORT, + config_entries.SOURCE_USER, + ], +) +async def test_single_instance(hass, source): + """Test we only allow a single config flow.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 14eb2a1a16e..367d215862e 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -1,200 +1,111 @@ """Test cases for the switcher_kis component.""" from datetime import timedelta -from typing import Any, Generator from unittest.mock import patch -from aioswitcher.consts import COMMAND_ON -from aioswitcher.devices import SwitcherV2Device -from pytest import raises +import pytest -from homeassistant.components.switcher_kis import ( +from homeassistant import config_entries +from homeassistant.components.switcher_kis.const import ( DATA_DEVICE, DOMAIN, - SIGNAL_SWITCHER_DEVICE_UPDATE, + MAX_UPDATE_INTERVAL_SEC, ) -from homeassistant.components.switcher_kis.switch import ( - CONF_AUTO_OFF, - CONF_TIMER_MINUTES, - SERVICE_SET_AUTO_OFF_NAME, - SERVICE_TURN_ON_WITH_TIMER_NAME, -) -from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Context, HomeAssistant, callback -from homeassistant.exceptions import UnknownUser -from homeassistant.helpers.config_validation import time_period_str -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component -from homeassistant.util import dt +from homeassistant.util import dt, slugify -from .consts import ( - DUMMY_AUTO_OFF_SET, - DUMMY_DEVICE_ID, - DUMMY_DEVICE_NAME, - DUMMY_DEVICE_STATE, - DUMMY_ELECTRIC_CURRENT, - DUMMY_IP_ADDRESS, - DUMMY_MAC_ADDRESS, - DUMMY_PHONE_ID, - DUMMY_POWER_CONSUMPTION, - DUMMY_REMAINING_TIME, - DUMMY_TIMER_MINUTES_SET, - MANDATORY_CONFIGURATION, - SWITCH_ENTITY_ID, -) +from . import init_integration +from .consts import DUMMY_SWITCHER_DEVICES, YAML_CONFIG -from tests.common import MockUser, async_fire_time_changed +from tests.common import async_fire_time_changed -async def test_failed_config( - hass: HomeAssistant, mock_failed_bridge: Generator[None, Any, None] -) -> None: - """Test failed configuration.""" - assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) is False - - -async def test_minimal_config( - hass: HomeAssistant, mock_bridge: Generator[None, Any, None] -) -> None: - """Test setup with configuration minimal entries.""" - assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) - - -async def test_discovery_data_bucket( - hass: HomeAssistant, mock_bridge: Generator[None, Any, None] -) -> None: - """Test the event send with the updated device.""" - assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) - +@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True) +async def test_async_setup_yaml_config(hass, mock_bridge) -> None: + """Test setup started by configuration from YAML.""" + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG) await hass.async_block_till_done() - device = hass.data[DOMAIN].get(DATA_DEVICE) - assert device.device_id == DUMMY_DEVICE_ID - assert device.ip_addr == DUMMY_IP_ADDRESS - assert device.mac_addr == DUMMY_MAC_ADDRESS - assert device.name == DUMMY_DEVICE_NAME - assert device.state == DUMMY_DEVICE_STATE - assert device.remaining_time == DUMMY_REMAINING_TIME - assert device.auto_off_set == DUMMY_AUTO_OFF_SET - assert device.power_consumption == DUMMY_POWER_CONSUMPTION - assert device.electric_current == DUMMY_ELECTRIC_CURRENT - assert device.phone_id == DUMMY_PHONE_ID + assert mock_bridge.is_running is True + assert len(hass.data[DOMAIN]) == 2 + assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2 -async def test_set_auto_off_service( - hass: HomeAssistant, - mock_bridge: Generator[None, Any, None], - mock_api: Generator[None, Any, None], - hass_owner_user: MockUser, -) -> None: - """Test the set_auto_off service.""" - assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) - +@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True) +async def test_async_setup_user_config_flow(hass, mock_bridge) -> None: + """Test setup started by user config flow.""" + with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert hass.services.has_service(DOMAIN, SERVICE_SET_AUTO_OFF_NAME) + assert mock_bridge.is_running is True + assert len(hass.data[DOMAIN]) == 2 + assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2 - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - blocking=True, - context=Context(user_id=hass_owner_user.id), + +async def test_update_fail(hass, mock_bridge, caplog): + """Test entities state unavailable when updates fail..""" + await init_integration(hass) + assert mock_bridge + + mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES) + await hass.async_block_till_done() + + assert mock_bridge.is_running is True + assert len(hass.data[DOMAIN]) == 2 + assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2 + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC + 1) + ) + await hass.async_block_till_done() + + for device in DUMMY_SWITCHER_DEVICES: + assert ( + f"Device {device.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds" + in caplog.text + ) + + entity_id = f"switch.{slugify(device.name)}" + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + entity_id = f"sensor.{slugify(device.name)}_power_consumption" + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES) + await hass.async_block_till_done() + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC - 1) ) - with raises(UnknownUser) as unknown_user_exc: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - blocking=True, - context=Context(user_id="not_real_user"), - ) + for device in DUMMY_SWITCHER_DEVICES: + entity_id = f"switch.{slugify(device.name)}" + state = hass.states.get(entity_id) + assert state.state != STATE_UNAVAILABLE - assert unknown_user_exc.type is UnknownUser - - with patch( - "homeassistant.components.switcher_kis.switch.SwitcherV2Api.set_auto_shutdown" - ) as mock_set_auto_shutdown: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - ) - - await hass.async_block_till_done() - - mock_set_auto_shutdown.assert_called_once_with( - time_period_str(DUMMY_AUTO_OFF_SET) - ) + entity_id = f"sensor.{slugify(device.name)}_power_consumption" + state = hass.states.get(entity_id) + assert state.state != STATE_UNAVAILABLE -async def test_turn_on_with_timer_service( - hass: HomeAssistant, - mock_bridge: Generator[None, Any, None], - mock_api: Generator[None, Any, None], - hass_owner_user: MockUser, -) -> None: - """Test the set_auto_off service.""" - assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) +async def test_entry_unload(hass, mock_bridge): + """Test entry unload.""" + entry = await init_integration(hass) + assert mock_bridge + assert entry.state is ConfigEntryState.LOADED + assert mock_bridge.is_running is True + assert len(hass.data[DOMAIN]) == 2 + + await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert hass.services.has_service(DOMAIN, SERVICE_TURN_ON_WITH_TIMER_NAME) - - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON_WITH_TIMER_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET}, - blocking=True, - context=Context(user_id=hass_owner_user.id), - ) - - with raises(UnknownUser) as unknown_user_exc: - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON_WITH_TIMER_NAME, - { - CONF_ENTITY_ID: SWITCH_ENTITY_ID, - CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, - }, - blocking=True, - context=Context(user_id="not_real_user"), - ) - - assert unknown_user_exc.type is UnknownUser - - with patch( - "homeassistant.components.switcher_kis.switch.SwitcherV2Api.control_device" - ) as mock_control_device: - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON_WITH_TIMER_NAME, - { - CONF_ENTITY_ID: SWITCH_ENTITY_ID, - CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, - }, - ) - - await hass.async_block_till_done() - - mock_control_device.assert_called_once_with( - COMMAND_ON, int(DUMMY_TIMER_MINUTES_SET) - ) - - -async def test_signal_dispatcher( - hass: HomeAssistant, mock_bridge: Generator[None, Any, None] -) -> None: - """Test signal dispatcher dispatching device updates every 4 seconds.""" - assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) - - await hass.async_block_till_done() - - @callback - def verify_update_data(device: SwitcherV2Device) -> None: - """Use as callback for signal dispatcher.""" - pass - - async_dispatcher_connect(hass, SIGNAL_SWITCHER_DEVICE_UPDATE, verify_update_data) - - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=5)) + assert entry.state is ConfigEntryState.NOT_LOADED + assert mock_bridge.is_running is False + assert len(hass.data[DOMAIN]) == 0 diff --git a/tests/components/switcher_kis/test_sensor.py b/tests/components/switcher_kis/test_sensor.py new file mode 100644 index 00000000000..b6fc1f2a49e --- /dev/null +++ b/tests/components/switcher_kis/test_sensor.py @@ -0,0 +1,93 @@ +"""Test the Switcher Sensor Platform.""" +import pytest + +from homeassistant.components.switcher_kis.const import DATA_DEVICE, DOMAIN +from homeassistant.helpers import entity_registry as er +from homeassistant.util import slugify + +from . import init_integration +from .consts import DUMMY_PLUG_DEVICE, DUMMY_SWITCHER_DEVICES, DUMMY_WATER_HEATER_DEVICE + +DEVICE_SENSORS_TUPLE = ( + ( + DUMMY_PLUG_DEVICE, + [ + "power_consumption", + "electric_current", + ], + ), + ( + DUMMY_WATER_HEATER_DEVICE, + [ + "power_consumption", + "electric_current", + "remaining_time", + ], + ), +) + + +@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True) +async def test_sensor_platform(hass, mock_bridge): + """Test sensor platform.""" + await init_integration(hass) + assert mock_bridge + + assert mock_bridge.is_running is True + assert len(hass.data[DOMAIN]) == 2 + assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2 + + for device, sensors in DEVICE_SENSORS_TUPLE: + for sensor in sensors: + entity_id = f"sensor.{slugify(device.name)}_{sensor}" + state = hass.states.get(entity_id) + assert state.state == str(getattr(device, sensor)) + + +async def test_sensor_disabled(hass, mock_bridge): + """Test sensor disabled by default.""" + await init_integration(hass) + assert mock_bridge + + mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) + await hass.async_block_till_done() + + registry = er.async_get(hass) + device = DUMMY_WATER_HEATER_DEVICE + unique_id = f"{device.device_id}-{device.mac_address}-auto_off_set" + entity_id = f"sensor.{slugify(device.name)}_auto_shutdown" + entry = registry.async_get(entity_id) + + assert entry + assert entry.unique_id == unique_id + assert entry.disabled is True + assert entry.disabled_by == er.DISABLED_INTEGRATION + + # Test enabling entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + + assert updated_entry != entry + assert updated_entry.disabled is False + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) +async def test_sensor_update(hass, mock_bridge, monkeypatch): + """Test sensor update.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_WATER_HEATER_DEVICE + sensor = "power_consumption" + entity_id = f"sensor.{slugify(device.name)}_{sensor}" + + state = hass.states.get(entity_id) + assert state.state == str(getattr(device, sensor)) + + monkeypatch.setattr(device, sensor, 1431) + mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "1431" diff --git a/tests/components/switcher_kis/test_services.py b/tests/components/switcher_kis/test_services.py new file mode 100644 index 00000000000..9b0fcee27df --- /dev/null +++ b/tests/components/switcher_kis/test_services.py @@ -0,0 +1,162 @@ +"""Test the services for the Switcher integration.""" +from unittest.mock import patch + +from aioswitcher.api import Command +from aioswitcher.device import DeviceState +import pytest + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switcher_kis.const import ( + CONF_AUTO_OFF, + CONF_TIMER_MINUTES, + DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + SERVICE_TURN_ON_WITH_TIMER_NAME, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers.config_validation import time_period_str +from homeassistant.util import slugify + +from . import init_integration +from .consts import ( + DUMMY_AUTO_OFF_SET, + DUMMY_PLUG_DEVICE, + DUMMY_TIMER_MINUTES_SET, + DUMMY_WATER_HEATER_DEVICE, +) + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) +async def test_turn_on_with_timer_service(hass, mock_bridge, mock_api, monkeypatch): + """Test the turn on with timer service.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_WATER_HEATER_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + # Test initial state - off + monkeypatch.setattr(device, "device_state", DeviceState.OFF) + mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device" + ) as mock_control_device: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + { + ATTR_ENTITY_ID: entity_id, + CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, + }, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with( + Command.ON, int(DUMMY_TIMER_MINUTES_SET) + ) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) +async def test_set_auto_off_service(hass, mock_bridge, mock_api): + """Test the set auto off service.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_WATER_HEATER_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown" + ) as mock_set_auto_shutdown: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + {ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_set_auto_shutdown.assert_called_once_with( + time_period_str(DUMMY_AUTO_OFF_SET) + ) + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) +async def test_set_auto_off_service_fail(hass, mock_bridge, mock_api, caplog): + """Test set auto off service failed.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_WATER_HEATER_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown", + return_value=None, + ) as mock_set_auto_shutdown: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + {ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_set_auto_shutdown.assert_called_once_with( + time_period_str(DUMMY_AUTO_OFF_SET) + ) + assert ( + f"Call api for {device.name} failed, api: 'set_auto_shutdown'" + in caplog.text + ) + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True) +async def test_plug_unsupported_services(hass, mock_bridge, mock_api, caplog): + """Test plug device unsupported services.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_PLUG_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + # Turn on with timer + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + { + ATTR_ENTITY_ID: entity_id, + CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, + }, + blocking=True, + ) + + assert mock_api.call_count == 0 + assert ( + f"Service '{SERVICE_TURN_ON_WITH_TIMER_NAME}' is not supported by {device.name}" + in caplog.text + ) + + # Auto off + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + {ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, + blocking=True, + ) + + assert mock_api.call_count == 0 + assert ( + f"Service '{SERVICE_SET_AUTO_OFF_NAME}' is not supported by {device.name}" + in caplog.text + ) diff --git a/tests/components/switcher_kis/test_switch.py b/tests/components/switcher_kis/test_switch.py new file mode 100644 index 00000000000..a44e0c79611 --- /dev/null +++ b/tests/components/switcher_kis/test_switch.py @@ -0,0 +1,127 @@ +"""Test the Switcher switch platform.""" +from unittest.mock import patch + +from aioswitcher.api import Command, SwitcherBaseResponse +from aioswitcher.device import DeviceState +import pytest + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.util import slugify + +from . import init_integration +from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) +async def test_switch(hass, mock_bridge, mock_api, monkeypatch): + """Test the switch.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_WATER_HEATER_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + # Test initial state - on + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test state change on --> off + monkeypatch.setattr(device, "device_state", DeviceState.OFF) + mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Test turning on + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device", + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(Command.ON) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test turning off + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device" + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with(Command.OFF) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +@pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True) +async def test_switch_control_fail(hass, mock_bridge, mock_api, monkeypatch, caplog): + """Test switch control fail.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_PLUG_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + # Test initial state - off + monkeypatch.setattr(device, "device_state", DeviceState.OFF) + mock_bridge.mock_callbacks([DUMMY_PLUG_DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Test exception during turn on + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device", + side_effect=RuntimeError("fake error"), + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(Command.ON) + assert ( + f"Call api for {device.name} failed, api: 'control_device'" in caplog.text + ) + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + # Make device available again + mock_bridge.mock_callbacks([DUMMY_PLUG_DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Test error response during turn on + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device", + return_value=SwitcherBaseResponse(None), + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with(Command.ON) + assert ( + f"Call api for {device.name} failed, api: 'control_device'" in caplog.text + ) + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE From c35b5a1c6430915e7a3a4d4ce9ec880dd9fecd0a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 19 Jul 2021 15:54:38 +0200 Subject: [PATCH 370/818] Add sound pressure unit constants (dB + dBa) (#53159) --- homeassistant/components/awair/const.py | 3 ++- homeassistant/components/demo/__init__.py | 8 ++++++-- homeassistant/components/isy994/const.py | 6 ++++-- homeassistant/components/mysensors/sensor.py | 3 ++- homeassistant/components/netatmo/sensor.py | 3 ++- homeassistant/components/point/sensor.py | 3 ++- homeassistant/const.py | 7 +++++++ 7 files changed, 25 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 2853ef9dd6c..1841a167a50 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -18,6 +18,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, + SOUND_PRESSURE_WEIGHTED_DBA, TEMP_CELSIUS, ) @@ -72,7 +73,7 @@ SENSOR_TYPES = { API_SPL_A: { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:ear-hearing", - ATTR_UNIT: "dBa", + ATTR_UNIT: SOUND_PRESSURE_WEIGHTED_DBA, ATTR_LABEL: "Sound level", ATTR_UNIQUE_ID: "sound_level", }, diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index eef341a9c8b..db53c1c528f 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -2,7 +2,11 @@ import asyncio from homeassistant import bootstrap, config_entries -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + SOUND_PRESSURE_DB, +) import homeassistant.core as ha DOMAIN = "demo" @@ -119,7 +123,7 @@ async def async_setup(hass, config): "min": 0, "max": 10, "name": "Allowed Noise", - "unit_of_measurement": "dB", + "unit_of_measurement": SOUND_PRESSURE_DB, } } }, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index ed40c7eb289..34b89baad68 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -71,6 +71,8 @@ from homeassistant.const import ( PRESSURE_MBAR, SERVICE_LOCK, SERVICE_UNLOCK, + SOUND_PRESSURE_DB, + SOUND_PRESSURE_WEIGHTED_DBA, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, @@ -341,8 +343,8 @@ UOM_FRIENDLY_NAME = { "8": VOLUME_CUBIC_METERS, "9": TIME_DAYS, "10": TIME_DAYS, - "12": "dB", - "13": "dB A", + "12": SOUND_PRESSURE_DB, + "13": SOUND_PRESSURE_WEIGHTED_DBA, "14": DEGREE, "16": "macroseismic", "17": TEMP_FAHRENHEIT, diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 1cb2b632362..467ab761124 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( MASS_KILOGRAMS, PERCENTAGE, POWER_WATT, + SOUND_PRESSURE_DB, TEMP_CELSIUS, TEMP_FAHRENHEIT, VOLT, @@ -53,7 +54,7 @@ SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { "V_FLOW": [LENGTH_METERS, "mdi:gauge", None], "V_VOLUME": [f"{VOLUME_CUBIC_METERS}", None, None], "V_LEVEL": { - "S_SOUND": ["dB", "mdi:volume-high", None], + "S_SOUND": [SOUND_PRESSURE_DB, "mdi:volume-high", None], "S_VIBRATION": [FREQUENCY_HERTZ, None, None], "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny", None], }, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index a6ba3fd1c57..035aa711a25 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + SOUND_PRESSURE_DB, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) @@ -92,7 +93,7 @@ SENSOR_TYPES = { None, False, ], - "noise": ["Noise", "Noise", "dB", "mdi:volume-high", None, True], + "noise": ["Noise", "Noise", SOUND_PRESSURE_DB, "mdi:volume-high", None, True], "humidity": ["Humidity", "Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, True], "rain": ["Rain", "Rain", LENGTH_MILLIMETERS, "mdi:weather-rainy", None, True], "sum_rain_1": [ diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 338ed275f50..dc34e1f9367 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_HPA, + SOUND_PRESSURE_WEIGHTED_DBA, TEMP_CELSIUS, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -24,7 +25,7 @@ SENSOR_TYPES = { DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS), DEVICE_CLASS_PRESSURE: (None, 0, PRESSURE_HPA), DEVICE_CLASS_HUMIDITY: (None, 1, PERCENTAGE), - DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, "dBa"), + DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, SOUND_PRESSURE_WEIGHTED_DBA), } diff --git a/homeassistant/const.py b/homeassistant/const.py index 3f07196d570..4e78e657ba6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -467,6 +467,13 @@ PRESSURE_MBAR: Final[UnitPressureT] = UnitPressureT(UnitT("mbar")) PRESSURE_INHG: Final[UnitPressureT] = UnitPressureT(UnitT("inHg")) PRESSURE_PSI: Final[UnitPressureT] = UnitPressureT(UnitT("psi")) +# Sound pressure units +UnitSoundPressureT = NewType("UnitSoundPressureT", UnitT) +SOUND_PRESSURE_DB: Final[UnitSoundPressureT] = UnitSoundPressureT(UnitT("dB")) +SOUND_PRESSURE_WEIGHTED_DBA: Final[UnitSoundPressureT] = UnitSoundPressureT( + UnitT("dBa") +) + # Volume units UnitVolumeT = NewType("UnitVolumeT", UnitT) VOLUME_LITERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("L")) From f6b162bc396e498c38d3078e080dc001de9c3a17 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 19 Jul 2021 15:57:06 +0200 Subject: [PATCH 371/818] Add pylint CodeStyle extension (#53147) --- .../components/alarmdecoder/config_flow.py | 4 ++-- homeassistant/components/apple_tv/config_flow.py | 2 +- homeassistant/components/aprs/device_tracker.py | 2 +- homeassistant/components/arcam_fmj/media_player.py | 2 +- .../components/bmw_connected_drive/sensor.py | 12 ++++++------ homeassistant/components/climacell/weather.py | 2 +- homeassistant/components/configurator/__init__.py | 4 ++-- homeassistant/components/daikin/climate.py | 2 +- homeassistant/components/deconz/services.py | 2 +- .../components/devolo_home_control/sensor.py | 2 +- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/evohome/__init__.py | 4 ++-- homeassistant/components/flunearyou/__init__.py | 2 +- homeassistant/components/fritz/services.py | 4 ++-- homeassistant/components/geniushub/__init__.py | 2 +- homeassistant/components/guardian/__init__.py | 4 ++-- homeassistant/components/guardian/switch.py | 4 ++-- homeassistant/components/harmony/__init__.py | 2 +- homeassistant/components/history_stats/sensor.py | 2 +- homeassistant/components/homekit/type_cameras.py | 2 +- homeassistant/components/hyperion/light.py | 4 ++-- .../components/image_processing/__init__.py | 2 +- homeassistant/components/incomfort/__init__.py | 2 +- homeassistant/components/insteon/climate.py | 4 ++-- homeassistant/components/iqvia/__init__.py | 4 ++-- homeassistant/components/isy994/__init__.py | 4 ++-- homeassistant/components/konnected/__init__.py | 2 +- homeassistant/components/lifx/light.py | 4 ++-- homeassistant/components/mqtt/fan.py | 2 +- homeassistant/components/mqtt/humidifier.py | 2 +- homeassistant/components/mvglive/sensor.py | 2 +- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/netatmo/config_flow.py | 2 +- homeassistant/components/netatmo/sensor.py | 4 ++-- homeassistant/components/openuv/__init__.py | 4 ++-- .../components/persistent_notification/__init__.py | 4 ++-- homeassistant/components/plex/media_player.py | 4 ++-- homeassistant/components/rainmachine/__init__.py | 4 ++-- homeassistant/components/rainmachine/switch.py | 4 ++-- homeassistant/components/rmvtransport/sensor.py | 2 +- homeassistant/components/roomba/irobot_base.py | 2 +- homeassistant/components/simplisafe/__init__.py | 4 ++-- homeassistant/components/solaredge/sensor.py | 14 +++++++------- homeassistant/components/sonos/__init__.py | 4 ++-- homeassistant/components/telegram_bot/__init__.py | 6 +++--- homeassistant/components/toon/binary_sensor.py | 4 ++-- homeassistant/components/toon/sensor.py | 4 ++-- homeassistant/components/tplink/common.py | 2 +- homeassistant/components/uk_transport/sensor.py | 4 ++-- homeassistant/components/universal/media_player.py | 2 +- .../components/waze_travel_time/sensor.py | 4 ++-- homeassistant/components/wsdot/sensor.py | 4 ++-- homeassistant/components/xiaomi_aqara/switch.py | 2 +- homeassistant/components/xiaomi_miio/switch.py | 2 +- homeassistant/components/zamg/sensor.py | 2 +- homeassistant/config.py | 4 ++-- homeassistant/helpers/script.py | 2 +- homeassistant/helpers/service.py | 2 +- homeassistant/util/unit_system.py | 4 ++-- pyproject.toml | 5 +++++ 60 files changed, 103 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py index cc4a19e4755..45d04feb3b2 100644 --- a/homeassistant/components/alarmdecoder/config_flow.py +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -299,7 +299,7 @@ def _validate_zone_input(zone_input): errors["base"] = "relay_inclusive" # The following keys must be int - for key in [CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]: + for key in (CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN): if key in zone_input: try: int(zone_input[key]) @@ -328,7 +328,7 @@ def _fix_input_types(zone_input): strings and then convert them to ints. """ - for key in [CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]: + for key in (CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN): if key in zone_input: zone_input[key] = int(zone_input[key]) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 9afcb7a61ca..56ad1e83e23 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -56,7 +56,7 @@ async def device_scan(identifier, loop, cache=None): if matches: return cache, matches[0] - for hosts in [_host_filter(), None]: + for hosts in (_host_filter(), None): scan_result = await scan(loop, timeout=3, hosts=hosts) matches = [atv for atv in scan_result if _filter_device(atv)] diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index ef3686fcd00..f86a5a4648d 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -178,7 +178,7 @@ class AprsListenerThread(threading.Thread): _LOGGER.warning( "APRS message contained invalid posambiguity: %s", str(pos_amb) ) - for attr in [ATTR_ALTITUDE, ATTR_COMMENT, ATTR_COURSE, ATTR_SPEED]: + for attr in (ATTR_ALTITUDE, ATTR_COMMENT, ATTR_COURSE, ATTR_SPEED): if attr in msg: attrs[attr] = msg[attr] diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 89f0cc3b112..e9e5e29a1c7 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -52,7 +52,7 @@ async def async_setup_entry( State(client, zone), config_entry.unique_id or config_entry.entry_id, ) - for zone in [1, 2] + for zone in (1, 2) ], True, ) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 053a5adff22..8d44d1290dc 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -451,12 +451,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "chargecycle_range", "total_electric_distance", ): - for attr in [ + for attr in ( "community_average", "community_high", "community_low", "user_average", - ]: + ): device = BMWConnectedDriveSensor( account, vehicle, @@ -466,7 +466,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) entities.append(device) if attribute_name == "chargecycle_range": - for attr in ["user_current_charge_cycle", "user_high"]: + for attr in ("user_current_charge_cycle", "user_high"): device = BMWConnectedDriveSensor( account, vehicle, @@ -476,7 +476,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) entities.append(device) if attribute_name == "total_electric_distance": - for attr in ["user_total"]: + for attr in ("user_total",): device = BMWConnectedDriveSensor( account, vehicle, @@ -593,13 +593,13 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): self._state = getattr(vehicle_last_trip, self._attribute) elif self._service == SERVICE_ALL_TRIPS: vehicle_all_trips = self._vehicle.state.all_trips - for attribute in [ + for attribute in ( "average_combined_consumption", "average_electric_consumption", "average_recuperation", "chargecycle_range", "total_electric_distance", - ]: + ): if self._attribute.startswith(f"{attribute}_"): attr = getattr(vehicle_all_trips, attribute) sub_attr = self._attribute.replace(f"{attribute}_", "") diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index be03b53ef72..2043459c496 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -109,7 +109,7 @@ async def async_setup_entry( api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity entities = [ api_class(config_entry, coordinator, api_version, forecast_type) - for forecast_type in [DAILY, HOURLY, NOWCAST] + for forecast_type in (DAILY, HOURLY, NOWCAST) ] async_add_entities(entities) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index e988e58f76b..f06ec330815 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -166,10 +166,10 @@ class Configurator: data.update( { key: value - for key, value in [ + for key, value in ( (ATTR_DESCRIPTION, description), (ATTR_SUBMIT_CAPTION, submit_caption), - ] + ) if value is not None } ) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index d17c3fc0d93..b3e833bb64f 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -135,7 +135,7 @@ class DaikinClimate(ClimateEntity): """Set device settings using API.""" values = {} - for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_HVAC_MODE]: + for attr in (ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_HVAC_MODE): value = settings.get(attr) if value is None: continue diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index a4f4aec6a76..08ee9e11561 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -153,7 +153,7 @@ async def async_refresh_devices_service(gateway): await gateway.api.refresh_state() gateway.ignore_state_updates = False - for new_device_type in [NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR]: + for new_device_type in (NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR): gateway.async_add_device_callback(new_device_type, force=True) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 3ab449b2c91..0500fc72b0b 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -52,7 +52,7 @@ async def async_setup_entry( for device in gateway.devices.values(): if hasattr(device, "consumption_property"): for consumption in device.consumption_property: - for consumption_type in ["current", "total"]: + for consumption_type in ("current", "total"): entities.append( DevoloConsumptionEntity( homecontrol=gateway, diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index f0579ef900b..d5964d5aea0 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -195,7 +195,7 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): options = dict(entry.options) modified = False - for importable_option in [CONF_EVENTS]: + for importable_option in (CONF_EVENTS,): if importable_option not in entry.options and importable_option in entry.data: options[importable_option] = entry.data[importable_option] modified = True diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 4084045b1fb..045a742485b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -653,10 +653,10 @@ class EvoChild(EvoDevice): this_sp_day = -1 if sp_idx == -1 else 0 next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - for key, offset, idx in [ + for key, offset, idx in ( ("this", this_sp_day, sp_idx), ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: + ): sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] switchpoint = day["Switchpoints"][idx] diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index d9203ed0c29..d80591c067c 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass, entry): raise UpdateFailed(err) from err data_init_tasks = [] - for api_category in [CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT]: + for api_category in (CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT): coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ api_category ] = DataUpdateCoordinator( diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index fcfbd54b743..359e7ced239 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Fritz integration.""" - for service in [SERVICE_REBOOT, SERVICE_RECONNECT]: + for service in (SERVICE_REBOOT, SERVICE_RECONNECT): if hass.services.has_service(DOMAIN, service): return @@ -34,7 +34,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: fritz_tools = hass.data[DOMAIN][entry] await fritz_tools.service_fritzbox(service_call.service) - for service in [SERVICE_REBOOT, SERVICE_RECONNECT]: + for service in (SERVICE_REBOOT, SERVICE_RECONNECT): hass.services.async_register(DOMAIN, service, async_call_fritz_service) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index bf5fc03ded5..cad80e8d707 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -120,7 +120,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) - for platform in ["climate", "water_heater", "sensor", "binary_sensor", "switch"]: + for platform in ("climate", "water_heater", "sensor", "binary_sensor", "switch"): hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) setup_service_functions(hass, broker) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 0ff488161ed..8e605ca121c 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -60,13 +60,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up DataUpdateCoordinators for the valve controller: init_valve_controller_tasks = [] - for api, api_coro in [ + for api, api_coro in ( (API_SENSOR_PAIR_DUMP, client.sensor.pair_dump), (API_SYSTEM_DIAGNOSTICS, client.system.diagnostics), (API_SYSTEM_ONBOARD_SENSOR_STATUS, client.system.onboard_sensor_status), (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), - ]: + ): coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ api ] = GuardianDataUpdateCoordinator( diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 29069c1abc5..ef74a35147f 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -44,7 +44,7 @@ async def async_setup_entry( """Set up Guardian switches based on a config entry.""" platform = entity_platform.async_get_current_platform() - for service_name, schema, method in [ + for service_name, schema, method in ( (SERVICE_DISABLE_AP, {}, "async_disable_ap"), (SERVICE_ENABLE_AP, {}, "async_enable_ap"), (SERVICE_PAIR_SENSOR, {vol.Required(CONF_UID): cv.string}, "async_pair_sensor"), @@ -64,7 +64,7 @@ async def async_setup_entry( {vol.Required(CONF_UID): cv.string}, "async_unpair_sensor", ), - ]: + ): platform.async_register_entity_service(service_name, schema, method) async_add_entities( diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index e76e5559f9d..c541aa0e0e3 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -94,7 +94,7 @@ async def _migrate_old_unique_ids( def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): options = dict(entry.options) modified = 0 - for importable_option in [ATTR_ACTIVITY, ATTR_DELAY_SECS]: + for importable_option in (ATTR_ACTIVITY, ATTR_DELAY_SECS): if importable_option not in entry.options and importable_option in entry.data: options[importable_option] = entry.data[importable_option] modified = 1 diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 69f42da5e36..e8ff9afc4e3 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -86,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensor_type = config.get(CONF_TYPE) name = config.get(CONF_NAME) - for template in [start, end]: + for template in (start, end): if template is not None: template.hass = hass diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 040c4017bb3..077366870e2 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -451,7 +451,7 @@ class Camera(HomeAccessory, PyhapCamera): _LOGGER.info("[%s] Stream already stopped", session_id) return True - for shutdown_method in ["close", "kill"]: + for shutdown_method in ("close", "kill"): _LOGGER.info("[%s] %s stream", session_id, shutdown_method) try: await getattr(stream, shutdown_method)() diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 63e90a89068..e9d23b4077e 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -527,10 +527,10 @@ class HyperionLight(HyperionBaseLight): # color, effect), but this is not possible due to: # https://github.com/hyperion-project/hyperion.ng/issues/967 if not bool(self._client.is_on()): - for component in [ + for component in ( const.KEY_COMPONENTID_ALL, const.KEY_COMPONENTID_LEDDEVICE, - ]: + ): if not await self._client.async_send_set_component( **{ const.KEY_COMPONENTSTATE: { diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 58a6582e33c..d1220a84cdd 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -164,7 +164,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): f_co = face[ATTR_CONFIDENCE] if f_co > confidence: confidence = f_co - for attr in [ATTR_NAME, ATTR_MOTION]: + for attr in (ATTR_NAME, ATTR_MOTION): if attr in face: state = face[attr] break diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index cea7244919b..9d14703fa32 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -53,7 +53,7 @@ async def async_setup(hass, hass_config): for heater in heaters: await heater.update() - for platform in ["water_heater", "binary_sensor", "sensor", "climate"]: + for platform in ("water_heater", "binary_sensor", "sensor", "climate"): hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) ) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 7e034311a82..dbfa3e8b2d9 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -224,7 +224,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): """Register INSTEON update events.""" await super().async_added_to_hass() await self._insteon_device.async_read_op_flags() - for group in [ + for group in ( COOLING, HEATING, DEHUMIDIFYING, @@ -236,5 +236,5 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): HUMIDITY, HUMIDITY_HIGH, HUMIDITY_LOW, - ]: + ): self._insteon_device.groups[group].subscribe(self.async_entity_update) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 30a54fa1fd0..fa783cc9031 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass, entry): raise UpdateFailed from err init_data_update_tasks = [] - for sensor_type, api_coro in [ + for sensor_type, api_coro in ( (TYPE_ALLERGY_FORECAST, client.allergens.extended), (TYPE_ALLERGY_INDEX, client.allergens.current), (TYPE_ALLERGY_OUTLOOK, client.allergens.outlook), @@ -67,7 +67,7 @@ async def async_setup_entry(hass, entry): (TYPE_ASTHMA_INDEX, client.asthma.current), (TYPE_DISEASE_FORECAST, client.disease.extended), (TYPE_DISEASE_INDEX, client.disease.current), - ]: + ): coordinator = coordinators[sensor_type] = DataUpdateCoordinator( hass, LOGGER, diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 51c34aeb0a7..e3d11efd739 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -244,11 +244,11 @@ def _async_import_options_from_data_if_missing( ): options = dict(entry.options) modified = False - for importable_option in [ + for importable_option in ( CONF_IGNORE_STRING, CONF_SENSOR_STRING, CONF_RESTORE_LIGHT_STATE, - ]: + ): if importable_option not in entry.options and importable_option in entry.data: options[importable_option] = entry.data[importable_option] modified = True diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 4b5890532d1..32d0f0e20c0 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -368,7 +368,7 @@ class KonnectedView(HomeAssistantView): zone_data["device_id"] = device_id - for attr in ["state", "temp", "humi", "addr"]: + for attr in ("state", "temp", "humi", "addr"): value = payload.get(attr) handler = HANDLERS.get(attr) if value is not None and handler: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 167a1ceee0a..30c0ffbe850 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -275,12 +275,12 @@ class LIFXManager: for discovery in self.discoveries: discovery.cleanup() - for service in [ + for service in ( SERVICE_LIFX_SET_STATE, SERVICE_EFFECT_STOP, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_COLORLOOP, - ]: + ): self.hass.services.async_remove(LIFX_DOMAIN, service) def register_set_state(self): diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index ef996a3c4ba..552ee8da6d6 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -360,7 +360,7 @@ class MqttFan(MqttEntity, FanEntity): if self._feature_preset_mode: self._supported_features |= SUPPORT_PRESET_MODE - for tpl_dict in [self._command_templates, self._value_templates]: + for tpl_dict in (self._command_templates, self._value_templates): for key, tpl in tpl_dict.items(): if tpl is None: tpl_dict[key] = lambda value: value diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 4e7e9ee5879..2f76b416184 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -237,7 +237,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None - for tpl_dict in [self._command_templates, self._value_templates]: + for tpl_dict in (self._command_templates, self._value_templates): for key, tpl in tpl_dict.items(): if tpl is None: tpl_dict[key] = lambda value: value diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 16b061a3346..953fe4c69a8 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -202,7 +202,7 @@ class MVGLiveData: # now select the relevant data _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} - for k in ["destination", "linename", "time", "direction", "product"]: + for k in ("destination", "linename", "time", "direction", "product"): _nextdep[k] = _departure.get(k, "") _nextdep["time"] = int(_nextdep["time"]) self.departures.append(_nextdep) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 97d54fb0669..52e506fc4dd 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Remove air_quality entities from registry if they exist ent_reg = entity_registry.async_get(hass) - for sensor_type in ["sds", ATTR_SDS011, ATTR_SPS30]: + for sensor_type in ("sds", ATTR_SDS011, ATTR_SPS30): unique_id = f"{coordinator.unique_id}-{sensor_type}" if entity_id := ent_reg.async_get_entity_id( AIR_QUALITY_PLATFORM, DOMAIN, unique_id diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index ea44339b99f..909255aa38e 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -191,7 +191,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): def fix_coordinates(user_input): """Fix coordinates if they don't comply with the Netatmo API.""" # Ensure coordinates have acceptable length for the Netatmo API - for coordinate in [CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW]: + for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW): if len(str(user_input[coordinate]).split(".")[1]) < 7: user_input[coordinate] = user_input[coordinate] + 0.0000001 diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 035aa711a25..917d04d1b4d 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -247,10 +247,10 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Adding weather sensors %s", entities) return entities - for data_class_name in [ + for data_class_name in ( WEATHERSTATION_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, - ]: + ): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 72bdbbd179a..df63dd91b2e 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -92,11 +92,11 @@ async def async_setup_entry(hass, config_entry): await openuv.async_update_protection_data() async_dispatcher_send(hass, TOPIC_UPDATE) - for service, method in [ + for service, method in ( ("update_data", update_data), ("update_uv_index_data", update_uv_index_data), ("update_protection_data", update_protection_data), - ]: + ): hass.services.async_register(DOMAIN, service, method) return True diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 071261e7b23..4a68dd3356f 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -80,11 +80,11 @@ def async_create( """Generate a notification.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_TITLE, title), (ATTR_MESSAGE, message), (ATTR_NOTIFICATION_ID, notification_id), - ] + ) if value is not None } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1da71bda6aa..1033c4286ac 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -509,13 +509,13 @@ class PlexMediaPlayer(MediaPlayerEntity): def extra_state_attributes(self): """Return the scene state attributes.""" attributes = {} - for attr in [ + for attr in ( "media_content_rating", "media_library_title", "player_source", "media_summary", "username", - ]: + ): value = getattr(self, attr, None) if value: attributes[attr] = value diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 6a239bc84a1..13540c092fa 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -129,13 +129,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(err) from err controller_init_tasks = [] - for api_category in [ + for api_category in ( DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, - ]: + ): coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ api_category ] = DataUpdateCoordinator( diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 68ecd8c8d64..8338e4c8305 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -123,7 +123,7 @@ async def async_setup_entry( alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} - for service_name, schema, method in [ + for service_name, schema, method in ( ("disable_program", alter_program_schema, "async_disable_program"), ("disable_zone", alter_zone_schema, "async_disable_zone"), ("enable_program", alter_program_schema, "async_enable_program"), @@ -156,7 +156,7 @@ async def async_setup_entry( ), ("stop_zone", {vol.Required(CONF_ZONE_ID): cv.positive_int}, "async_stop_zone"), ("unpause_watering", {}, "async_unpause_watering"), - ]: + ): platform.async_register_entity_service(service_name, schema, method) controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index d85dae53303..9e4e7f3d588 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -262,7 +262,7 @@ class RMVDepartureData: elif journey["minutes"] < self._time_offset: continue - for attr in ["direction", "departure_time", "product", "minutes"]: + for attr in ("direction", "departure_time", "product", "minutes"): _nextdep[attr] = journey.get(attr, "") _nextdep["line"] = journey.get("number", "") diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 2f57ef954b6..8cee2a1ce61 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -212,7 +212,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): pos_x = pos_state.get("point", {}).get("x") pos_y = pos_state.get("point", {}).get("y") theta = pos_state.get("theta") - if all(item is not None for item in [pos_x, pos_y, theta]): + if all(item is not None for item in (pos_x, pos_y, theta)): position = f"({pos_x}, {pos_y}, {theta})" state_attrs[ATTR_POSITION] = position diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b3f246b1847..baf1bcd7b2a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -261,7 +261,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 LOGGER.error("Error during service call: %s", err) return - for service, method, schema in [ + for service, method, schema in ( ("clear_notifications", clear_notifications, None), ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), @@ -270,7 +270,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 set_system_properties, SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA, ), - ]: + ): async_register_admin_service(hass, DOMAIN, service, method, schema=schema) config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 340b6e0c2c9..23cfa116599 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -72,31 +72,31 @@ class SolarEdgeSensorFactory: ], ] = {"site_details": (SolarEdgeDetailsSensor, details)} - for key in [ + for key in ( "lifetime_energy", "energy_this_year", "energy_this_month", "energy_today", "current_power", - ]: + ): self.services[key] = (SolarEdgeOverviewSensor, overview) - for key in ["meters", "sensors", "gateways", "batteries", "inverters"]: + for key in ("meters", "sensors", "gateways", "batteries", "inverters"): self.services[key] = (SolarEdgeInventorySensor, inventory) - for key in ["power_consumption", "solar_power", "grid_power", "storage_power"]: + for key in ("power_consumption", "solar_power", "grid_power", "storage_power"): self.services[key] = (SolarEdgePowerFlowSensor, flow) - for key in ["storage_level"]: + for key in ("storage_level",): self.services[key] = (SolarEdgeStorageLevelSensor, flow) - for key in [ + for key in ( "purchased_power", "production_power", "feedin_power", "consumption_power", "selfconsumption_power", - ]: + ): self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) def create_sensor(self, sensor_type: SolarEdgeSensor) -> SolarEdgeSensor: diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index cf4592ff4b9..ec26373c44e 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -190,10 +190,10 @@ class SonosDiscoveryManager: _LOGGER.debug("Adding new speaker: %s", speaker_info) speaker = SonosSpeaker(self.hass, soco, speaker_info) self.data.discovered[soco.uid] = speaker - for coordinator, coord_dict in [ + for coordinator, coord_dict in ( (SonosAlarms, self.data.alarms), (SonosFavorites, self.data.favorites), - ]: + ): if soco.household_id not in coord_dict: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 4231bcc46af..c3f07e5269d 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -332,7 +332,7 @@ async def async_setup(hass, config): attribute_templ = data.get(attribute) if attribute_templ: if any( - isinstance(attribute_templ, vtype) for vtype in [float, int, str] + isinstance(attribute_templ, vtype) for vtype in (float, int, str) ): data[attribute] = attribute_templ else: @@ -352,7 +352,7 @@ async def async_setup(hass, config): msgtype = service.service kwargs = dict(service.data) - for attribute in [ + for attribute in ( ATTR_MESSAGE, ATTR_TITLE, ATTR_URL, @@ -360,7 +360,7 @@ async def async_setup(hass, config): ATTR_CAPTION, ATTR_LONGITUDE, ATTR_LATITUDE, - ]: + ): _render_template_attr(kwargs, attribute) _LOGGER.debug("New telegram message %s: %s", msgtype, kwargs) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index de756225d57..9983dc4bee6 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -42,14 +42,14 @@ async def async_setup_entry( sensors.extend( [ ToonBoilerBinarySensor(coordinator, key=key) - for key in [ + for key in ( "thermostat_info_ot_communication_error_0", "thermostat_info_error_found_255", "thermostat_info_burner_info_None", "thermostat_info_burner_info_1", "thermostat_info_burner_info_2", "thermostat_info_burner_info_3", - ] + ) ] ) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 90f74ceae87..b16672674af 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -89,7 +89,7 @@ async def async_setup_entry( sensors.extend( [ ToonSolarDeviceSensor(coordinator, key=key) - for key in [ + for key in ( "solar_value", "solar_maximum", "solar_produced", @@ -98,7 +98,7 @@ async def async_setup_entry( "power_usage_day_from_grid_usage", "power_usage_day_to_grid_usage", "power_usage_current_covered_by_solar", - ] + ) ] ) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 3096281776a..84ce7edadb6 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -136,7 +136,7 @@ def get_static_devices(config_data) -> SmartDevices: lights = [] switches = [] - for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER]: + for type_ in (CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER): for entry in config_data[type_]: host = entry["host"] try: diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 41549c202b3..c66db9bb24b 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -183,12 +183,12 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): """Return other details about the sensor state.""" attrs = {} if self._data is not None: - for key in [ + for key in ( ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME, ATTR_REQUEST_TIME, - ]: + ): attrs[key] = self._data.get(key) attrs[ATTR_NEXT_BUSES] = self._next_buses return attrs diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 17dcde55381..a2981852dc1 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -472,7 +472,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_MEDIA_PREVIOUS_TRACK in self._cmds: flags |= SUPPORT_PREVIOUS_TRACK - if any(cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]): + if any(cmd in self._cmds for cmd in (SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN)): flags |= SUPPORT_VOLUME_STEP if SERVICE_VOLUME_SET in self._cmds: flags |= SUPPORT_VOLUME_SET diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 4fb56700f59..b0168bbb44e 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -127,7 +127,7 @@ async def async_setup_entry( if not config_entry.options: new_data = config_entry.data.copy() options = {} - for key in [ + for key in ( CONF_INCL_FILTER, CONF_EXCL_FILTER, CONF_REALTIME, @@ -136,7 +136,7 @@ async def async_setup_entry( CONF_AVOID_SUBSCRIPTION_ROADS, CONF_AVOID_FERRIES, CONF_UNITS, - ]: + ): if key in new_data: options[key] = new_data.pop(key) elif key in defaults: diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 8b45326cdbd..153d496a7d6 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -122,12 +122,12 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Return other details about the sensor state.""" if self._data is not None: attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} - for key in [ + for key in ( ATTR_AVG_TIME, ATTR_NAME, ATTR_DESCRIPTION, ATTR_TRAVEL_TIME_ID, - ]: + ): attrs[key] = self._data.get(key) attrs[ATTR_TIME_UPDATED] = _parse_wsdot_timestamp( self._data.get(ATTR_TIME_UPDATED) diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 8b16b6491c7..c17cf080a60 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -194,7 +194,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): if not self._in_use: self._load_power = 0 - for key in [POWER_CONSUMED, ENERGY_CONSUMED]: + for key in (POWER_CONSUMED, ENERGY_CONSUMED): if key in data: self._power_consumed = round(float(data[key]), 2) break diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 28c4f79ef28..52682a05f08 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -181,7 +181,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # The device has two switchable channels (mains and a USB port). # A switch device per channel will be created. - for channel_usb in [True, False]: + for channel_usb in (True, False): if channel_usb: unique_id_ch = f"{unique_id}-USB" else: diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index b35f675440e..a6018de831e 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -283,7 +283,7 @@ def _get_zamg_stations(): try: stations[row["synnr"]] = tuple( float(row[coord].replace(",", ".")) - for coord in ["breite_dezi", "länge_dezi"] + for coord in ("breite_dezi", "länge_dezi") ) except KeyError: _LOGGER.error("ZAMG schema changed again, cannot autodetect station") diff --git a/homeassistant/config.py b/homeassistant/config.py index 5a8f71bda77..9e128fc6d23 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -511,7 +511,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non if any( k in config - for k in [ + for k in ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, @@ -520,7 +520,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non CONF_UNIT_SYSTEM, CONF_EXTERNAL_URL, CONF_INTERNAL_URL, - ] + ) ): hac.config_source = SOURCE_YAML diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 6108b4b8f65..addd2ae046d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -599,7 +599,7 @@ class _ScriptRun: """Fire an event.""" self._step_log(self._action.get(CONF_ALIAS, self._action[CONF_EVENT])) event_data = {} - for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]: + for conf in (CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE): if conf not in self._action: continue diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ff037998f34..ed07c6bda63 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -227,7 +227,7 @@ def async_prepare_call_from_config( service_data = {} - for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: + for conf in (CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE): if conf not in config: continue try: diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index e49c1895180..59dbf784152 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -91,13 +91,13 @@ class UnitSystem: """Initialize the unit system object.""" errors: str = ", ".join( UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type) - for unit, unit_type in [ + for unit, unit_type in ( (temperature, TEMPERATURE), (length, LENGTH), (volume, VOLUME), (mass, MASS), (pressure, PRESSURE), - ] + ) if not is_valid_unit(unit, unit_type) ) diff --git a/pyproject.toml b/pyproject.toml index ee6e0015a84..8ca6a06868f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ ignore = [ jobs = 2 init-hook='from pylint.config.find_default_config_files import find_default_config_files; from pathlib import Path; import sys; sys.path.append(str(Path(Path(list(find_default_config_files())[0]).parent, "pylint/plugins")))' load-plugins = [ + "pylint.extensions.code_style", "pylint.extensions.typing", "pylint_strict_informational", "hass_constructor", @@ -68,6 +69,9 @@ good-names = [ # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this +# --- +# Enable once current issues are fixed: +# consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension) disable = [ "format", "abstract-class-little-used", @@ -90,6 +94,7 @@ disable = [ "too-many-boolean-expressions", "unused-argument", "wrong-import-order", + "consider-using-namedtuple-or-dataclass", ] enable = [ #"useless-suppression", # temporarily every now and then to clean them up From 327208c943249eecdf69e3091230b9bb30e86d95 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Mon, 19 Jul 2021 11:07:15 -0400 Subject: [PATCH 372/818] Bugfix current temperature in gree climate (#53149) * Bugfix current temperature gree climate * Retry build * Update from the review --- homeassistant/components/gree/climate.py | 4 ++-- tests/components/gree/test_climate.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index acd57ef590d..73ea66e5895 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -159,8 +159,8 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): @property def current_temperature(self) -> float: - """Return the target temperature, gree devices don't provide internal temp.""" - return self.target_temperature + """Return the reported current temperature for the device.""" + return self.coordinator.device.current_temperature @property def target_temperature(self) -> float: diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index c062cfc5615..d88f6a6fbf0 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -7,6 +7,7 @@ from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError import pytest from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, @@ -379,16 +380,21 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] + "units,temperature", [(TEMP_CELSIUS, 26), (TEMP_FAHRENHEIT, 74)] ) async def test_send_target_temperature(hass, discovery, device, units, temperature): """Test for sending target temperature command to the device.""" hass.config.units.temperature_unit = units + + fake_device = device() if units == TEMP_FAHRENHEIT: - device().temperature_units = 1 + fake_device.temperature_units = 1 await async_setup_gree(hass) + # Make sure we're trying to test something that isn't the default + assert fake_device.current_temperature != temperature + assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -399,8 +405,13 @@ async def test_send_target_temperature(hass, discovery, device, units, temperatu state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_TEMPERATURE) == temperature + assert ( + state.attributes.get(ATTR_CURRENT_TEMPERATURE) + == fake_device.current_temperature + ) - # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + # Reset config temperature_unit back to CELSIUS, required for + # additional tests outside this component. hass.config.units.temperature_unit = TEMP_CELSIUS From 0b60b86917ffa1df609759f067be9a2edf24e7b4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 17:45:03 +0200 Subject: [PATCH 373/818] Correct typing in azure_devops and activate mypy (#53152) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/azure_devops/__init__.py | 2 +- homeassistant/components/azure_devops/sensor.py | 5 ++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index f5537a63291..d8b326bf2d5 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -85,7 +85,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): """Return device information about this Azure DevOps instance.""" return { "identifiers": { - ( + ( # type: ignore DOMAIN, self.organization, self.project, diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index d7589cf5014..ee3a0356a52 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -97,7 +97,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): "mdi:pipe", ) - async def _azure_devops_update(self) -> bool: + async def _azure_devops_update(self) -> None: """Update Azure DevOps entity.""" try: build: DevOpsBuild = await self.client.get_build( @@ -106,7 +106,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): except aiohttp.ClientError as exception: _LOGGER.warning(exception) self._attr_available = False - return False + return self._attr_state = build.build_number self._attr_extra_state_attributes = { "definition_id": build.definition.id, @@ -123,4 +123,3 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): "finish_time": build.finish_time, } self._attr_available = True - return True diff --git a/mypy.ini b/mypy.ini index 6245144f54f..e98094ce058 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1097,9 +1097,6 @@ ignore_errors = true [mypy-homeassistant.components.awair.*] ignore_errors = true -[mypy-homeassistant.components.azure_devops.*] -ignore_errors = true - [mypy-homeassistant.components.azure_event_hub.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f0f7abe7b7e..c4ff71299e3 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -24,7 +24,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.atag.*", "homeassistant.components.aurora.*", "homeassistant.components.awair.*", - "homeassistant.components.azure_devops.*", "homeassistant.components.azure_event_hub.*", "homeassistant.components.blueprint.*", "homeassistant.components.bmw_connected_drive.*", From 019568869dcc3991fcbef5d1951c1b464e8b8d0e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 19 Jul 2021 11:50:26 -0400 Subject: [PATCH 374/818] Use entity class attributes for avea (#52695) * Use entity class attributes for avea * fix pylint * redo brightness * redo brightness --- homeassistant/components/avea/light.py | 34 +++++--------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index eca020f6cd0..d1df7ba3e46 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -30,32 +30,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AveaLight(LightEntity): """Representation of an Avea.""" + _attr_supported_features = SUPPORT_AVEA + def __init__(self, light): """Initialize an AveaLight.""" self._light = light - self._name = light.name - self._state = None - self._brightness = light.brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_AVEA - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - - @property - def is_on(self): - """Return true if light is on.""" - return self._state + self._attr_name = light.name + self._attr_brightness = light.brightness def turn_on(self, **kwargs): """Instruct the light to turn on.""" @@ -80,8 +61,5 @@ class AveaLight(LightEntity): """ brightness = self._light.get_brightness() if brightness is not None: - if brightness == 0: - self._state = False - else: - self._state = True - self._brightness = round(255 * (brightness / 4095)) + self._attr_is_on = brightness != 0 + self._attr_brightness = round(255 * (brightness / 4095)) From 0865917eeb5299b4192488d468054666e77ea885 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 17:59:41 +0200 Subject: [PATCH 375/818] Activate mypy in aurora (#53150) --- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 2 files changed, 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index e98094ce058..c3c89a7b08d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1091,9 +1091,6 @@ ignore_errors = true [mypy-homeassistant.components.atag.*] ignore_errors = true -[mypy-homeassistant.components.aurora.*] -ignore_errors = true - [mypy-homeassistant.components.awair.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c4ff71299e3..407dec6849a 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -22,7 +22,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.analytics.*", "homeassistant.components.asuswrt.*", "homeassistant.components.atag.*", - "homeassistant.components.aurora.*", "homeassistant.components.awair.*", "homeassistant.components.azure_event_hub.*", "homeassistant.components.blueprint.*", From bf4ca2d68dee739bda0304829ab4fa2e8c2c1660 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 19 Jul 2021 10:01:36 -0600 Subject: [PATCH 376/818] Modify AirVisual states to be translatable (#53133) * Modify AirVisual states to be translatable * Make constant names consistent --- homeassistant/components/airvisual/sensor.py | 42 ++++++++++++------- .../components/airvisual/strings.sensor.json | 20 +++++++++ .../airvisual/translations/sensor.en.json | 20 +++++++++ 3 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/airvisual/strings.sensor.json create mode 100644 homeassistant/components/airvisual/translations/sensor.en.json diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index fb6cc0c5e26..796607f8215 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -37,6 +37,9 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol" ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_REGION = "region" +DEVICE_CLASS_POLLUTANT_LABEL = "airvisual__pollutant_label" +DEVICE_CLASS_POLLUTANT_LEVEL = "airvisual__pollutant_level" + SENSOR_KIND_AQI = "air_quality_index" SENSOR_KIND_BATTERY_LEVEL = "battery_level" SENSOR_KIND_CO2 = "carbon_dioxide" @@ -105,22 +108,27 @@ NODE_PRO_SENSORS = [ ), ] -POLLUTANT_LABELS = { - "co": "Carbon Monoxide", - "n2": "Nitrogen Dioxide", - "o3": "Ozone", - "p1": "PM10", - "p2": "PM2.5", - "s2": "Sulfur Dioxide", -} +STATE_POLLUTANT_LABEL_CO = "co" +STATE_POLLUTANT_LABEL_N2 = "n2" +STATE_POLLUTANT_LABEL_O3 = "o3" +STATE_POLLUTANT_LABEL_P1 = "p1" +STATE_POLLUTANT_LABEL_P2 = "p2" +STATE_POLLUTANT_LABEL_S2 = "s2" + +STATE_POLLUTANT_LEVEL_GOOD = "good" +STATE_POLLUTANT_LEVEL_MODERATE = "moderate" +STATE_POLLUTANT_LEVEL_UNHEALTHY_SENSITIVE = "unhealthy_sensitive" +STATE_POLLUTANT_LEVEL_UNHEALTHY = "unhealthy" +STATE_POLLUTANT_LEVEL_VERY_UNHEALTHY = "very_unhealthy" +STATE_POLLUTANT_LEVEL_HAZARDOUS = "hazardous" POLLUTANT_LEVELS = { - (0, 50): ("Good", "mdi:emoticon-excited"), - (51, 100): ("Moderate", "mdi:emoticon-happy"), - (101, 150): ("Unhealthy for sensitive groups", "mdi:emoticon-neutral"), - (151, 200): ("Unhealthy", "mdi:emoticon-sad"), - (201, 300): ("Very unhealthy", "mdi:emoticon-dead"), - (301, 1000): ("Hazardous", "mdi:biohazard"), + (0, 50): (STATE_POLLUTANT_LEVEL_GOOD, "mdi:emoticon-excited"), + (51, 100): (STATE_POLLUTANT_LEVEL_MODERATE, "mdi:emoticon-happy"), + (101, 150): (STATE_POLLUTANT_LEVEL_UNHEALTHY_SENSITIVE, "mdi:emoticon-neutral"), + (151, 200): (STATE_POLLUTANT_LEVEL_UNHEALTHY, "mdi:emoticon-sad"), + (201, 300): (STATE_POLLUTANT_LEVEL_VERY_UNHEALTHY, "mdi:emoticon-dead"), + (301, 1000): (STATE_POLLUTANT_LEVEL_HAZARDOUS, "mdi:biohazard"), } POLLUTANT_UNITS = { @@ -170,6 +178,10 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) + if kind == SENSOR_KIND_LEVEL: + self._attr_device_class = DEVICE_CLASS_POLLUTANT_LEVEL + elif kind == SENSOR_KIND_POLLUTANT: + self._attr_device_class = DEVICE_CLASS_POLLUTANT_LABEL self._attr_extra_state_attributes.update( { ATTR_CITY: config_entry.data.get(CONF_CITY), @@ -209,7 +221,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): self._attr_state = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: symbol = data[f"main{self._locale}"] - self._attr_state = POLLUTANT_LABELS[symbol] + self._attr_state = symbol self._attr_extra_state_attributes.update( { ATTR_POLLUTANT_SYMBOL: symbol, diff --git a/homeassistant/components/airvisual/strings.sensor.json b/homeassistant/components/airvisual/strings.sensor.json new file mode 100644 index 00000000000..583ddaf4f3b --- /dev/null +++ b/homeassistant/components/airvisual/strings.sensor.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Carbon Monoxide", + "n2": "Nitrogen Dioxide", + "o3": "Ozone", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Sulfur Dioxide" + }, + "airvisual__pollutant_level": { + "good": "Good", + "moderate": "Moderate", + "unhealthy": "Unhealthy", + "unhealthy_sensitive": "Unhealthy for sensitive groups", + "very_unhealthy": "Very unhealthy", + "hazardous": "Hazardous" + } + } +} diff --git a/homeassistant/components/airvisual/translations/sensor.en.json b/homeassistant/components/airvisual/translations/sensor.en.json new file mode 100644 index 00000000000..314cf34562a --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.en.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Carbon Monoxide", + "n2": "Nitrogen Dioxide", + "o3": "Ozone", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Sulfur Dioxide" + }, + "airvisual__pollutant_level": { + "good": "Good", + "hazardous": "Hazardous", + "moderate": "Moderate", + "unhealthy": "Unhealthy", + "unhealthy_sensitive": "Unhealthy for sensitive groups", + "very_unhealthy": "Very unhealthy" + } + } +} \ No newline at end of file From 3c6f0d11a64c2c82bab03a35cc4ecc75c8550774 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 19 Jul 2021 12:02:09 -0400 Subject: [PATCH 377/818] Use entity class attributes for Citybikes (#53167) * Use entity class attributes for citybikes * tweak --- homeassistant/components/citybikes/sensor.py | 50 +++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index bc323a51151..7d54d259051 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -265,50 +265,32 @@ class CityBikesNetwork: class CityBikesStation(SensorEntity): """CityBikes API Sensor.""" + _attr_unit_of_measurement = "bikes" + _attr_icon = "mdi:bike" + def __init__(self, network, station_id, entity_id): """Initialize the sensor.""" self._network = network self._station_id = station_id - self._station_data = {} self.entity_id = entity_id - @property - def state(self): - """Return the state of the sensor.""" - return self._station_data.get(ATTR_FREE_BIKES) - - @property - def name(self): - """Return the name of the sensor.""" - return self._station_data.get(ATTR_NAME) - async def async_update(self): """Update station state.""" for station in self._network.stations: if station[ATTR_ID] == self._station_id: - self._station_data = station + station_data = station break - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if self._station_data: - return { + self._attr_name = station_data.get(ATTR_NAME) + self._attr_state = station_data.get(ATTR_FREE_BIKES) + self._attr_extra_state_attributes = ( + { ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION, - ATTR_UID: self._station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), - ATTR_LATITUDE: self._station_data[ATTR_LATITUDE], - ATTR_LONGITUDE: self._station_data[ATTR_LONGITUDE], - ATTR_EMPTY_SLOTS: self._station_data[ATTR_EMPTY_SLOTS], - ATTR_TIMESTAMP: self._station_data[ATTR_TIMESTAMP], + ATTR_UID: station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), + ATTR_LATITUDE: station_data[ATTR_LATITUDE], + ATTR_LONGITUDE: station_data[ATTR_LONGITUDE], + ATTR_EMPTY_SLOTS: station_data[ATTR_EMPTY_SLOTS], + ATTR_TIMESTAMP: station_data[ATTR_TIMESTAMP], } - return {ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION} - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return "bikes" - - @property - def icon(self): - """Return the icon.""" - return "mdi:bike" + if station_data + else {ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION} + ) From dd8ec04e5800aeed41ed0aeb52bce84811bf224d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Jul 2021 18:03:02 +0200 Subject: [PATCH 378/818] Upgrade black to 21.7b0 (#53192) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 952df031661..1a2e6f45fdd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 21.7b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 589ecd666f8..9d1d068f7cd 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.6b0 +black==21.7b0 codespell==2.0.0 flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 From 225732d00e2f59ae23fce88975b0fb87d5f57bc4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 19 Jul 2021 11:50:04 -0500 Subject: [PATCH 379/818] Remove I/O in Plex tests (#53196) --- tests/components/plex/test_config_flow.py | 14 +++++++++++--- tests/components/plex/test_init.py | 13 +++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 716864c1cb1..72958fc10c0 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -624,7 +624,9 @@ async def test_manual_config(hass, mock_plex_calls, current_request_with_host): assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_manual_config_with_token(hass, mock_plex_calls): +async def test_manual_config_with_token( + hass, mock_plex_calls, requests_mock, empty_library, empty_payload +): """Test creating via manual configuration with only token.""" result = await hass.config_entries.flow.async_init( @@ -653,13 +655,19 @@ async def test_manual_config_with_token(hass, mock_plex_calls): server_id = result["data"][CONF_SERVER_IDENTIFIER] mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + mock_url = mock_plex_server.url_in_use - assert result["title"] == mock_plex_server.url_in_use + assert result["title"] == mock_url assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_url assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + # Complete Plex integration setup before teardown + requests_mock.get(f"{mock_url}/library", text=empty_library) + requests_mock.get(f"{mock_url}/library/sections", text=empty_payload) + await hass.async_block_till_done() + async def test_setup_with_limited_credentials(hass, entry, setup_plex_server): """Test setup with a user with limited permissions.""" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 530f265f3f0..081adb845f4 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -177,6 +177,7 @@ async def test_setup_with_unknown_session(hass, entry, setup_plex_server): async def test_setup_when_certificate_changed( hass, requests_mock, + empty_library, empty_payload, plex_server_accounts, plex_server_default, @@ -210,13 +211,10 @@ async def test_setup_when_certificate_changed( requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users) requests_mock.get("https://plex.tv/api/invites/requested", text=empty_payload) - - requests_mock.get("https://plex.tv/users/account", text=plextv_account) - requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) requests_mock.get(old_url, exc=WrongCertHostnameException) # Test with account failure - requests_mock.get(f"{old_url}/accounts", status_code=401) + requests_mock.get("https://plex.tv/users/account", status_code=401) old_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() @@ -225,7 +223,7 @@ async def test_setup_when_certificate_changed( await hass.config_entries.async_unload(old_entry.entry_id) # Test with no servers found - requests_mock.get(f"{old_url}/accounts", text=plex_server_accounts) + requests_mock.get("https://plex.tv/users/account", text=plextv_account) requests_mock.get("https://plex.tv/api/resources", text=empty_payload) assert await hass.config_entries.async_setup(old_entry.entry_id) is False @@ -237,8 +235,11 @@ async def test_setup_when_certificate_changed( # Test with success new_url = PLEX_DIRECT_URL requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) - requests_mock.get(new_url, text=plex_server_default) + for resource_url in [new_url, "http://1.2.3.4:32400"]: + requests_mock.get(resource_url, text=plex_server_default) requests_mock.get(f"{new_url}/accounts", text=plex_server_accounts) + requests_mock.get(f"{new_url}/library", text=empty_library) + requests_mock.get(f"{new_url}/library/sections", text=empty_payload) assert await hass.config_entries.async_setup(old_entry.entry_id) await hass.async_block_till_done() From 8743a03f14076599d33732728e0468eaad4f8dd6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Jul 2021 19:00:37 +0200 Subject: [PATCH 380/818] Upgrade numpy to 1.21.1 (#53194) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 817623a6895..3ae79664953 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.0"], + "requirements": ["numpy==1.21.1"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 84696f4e579..da50819c9a0 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.0", "pyiqvia==1.0.0"], + "requirements": ["numpy==1.21.1", "pyiqvia==1.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 4dcf61636ee..b7b8024009b 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.0", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.21.1", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e16da5d2ab2..b3162a19364 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.21.0", + "numpy==1.21.1", "pillow==8.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 75887e5e78f..c6502bf0850 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.0"], + "requirements": ["numpy==1.21.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 516885a9bb0..60e206b809f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1049,7 +1049,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.0 +numpy==1.21.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1ffcb18d5d..5985da6e5c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -588,7 +588,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.0 +numpy==1.21.1 # homeassistant.components.google oauth2client==4.0.0 From 3d40fdf2c66b162345ba849fa1c0b82d795d3f97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Jul 2021 19:01:19 +0200 Subject: [PATCH 381/818] Upgrade holidays to 0.11.2 (#53191) --- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 6fc8d2328a1..f43003738df 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.11.1"], + "requirements": ["holidays==0.11.2"], "codeowners": ["@fabaff"], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 60e206b809f..d52b4ca5414 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -772,7 +772,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.11.1 +holidays==0.11.2 # homeassistant.components.frontend home-assistant-frontend==20210707.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5985da6e5c5..7674a78ce9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.11.1 +holidays==0.11.2 # homeassistant.components.frontend home-assistant-frontend==20210707.0 From d4589894fe51007be1a71b7b17574de944b741fb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Jul 2021 19:30:52 +0200 Subject: [PATCH 382/818] Correct typing in bsblan and activate mypy (#53153) --- homeassistant/components/bsblan/climate.py | 5 ++-- homeassistant/components/bsblan/const.py | 33 +++++++++++----------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index d6ec805af55..32473eabce1 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, + UnitTemperatureT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -100,7 +101,7 @@ class BSBLanClimate(ClimateEntity): self._hvac_mode: str | None = None self._target_temperature: float | None = None self._temperature_unit = None - self._preset_mode = None + self._preset_mode: str | None = None self._store_hvac_mode = None self._info: Info = info self.bsblan = bsblan @@ -121,7 +122,7 @@ class BSBLanClimate(ClimateEntity): return self._info.device_identification @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> UnitTemperatureT: """Return the unit of measurement which this thermostat uses.""" if self._temperature_unit == "°C": return TEMP_CELSIUS diff --git a/homeassistant/components/bsblan/const.py b/homeassistant/components/bsblan/const.py index 1dd461e2081..e65100af90d 100644 --- a/homeassistant/components/bsblan/const.py +++ b/homeassistant/components/bsblan/const.py @@ -1,26 +1,27 @@ """Constants for the BSB-Lan integration.""" +from typing import Final DOMAIN = "bsblan" -DATA_BSBLAN_CLIENT = "bsblan_client" -DATA_BSBLAN_TIMER = "bsblan_timer" -DATA_BSBLAN_UPDATED = "bsblan_updated" +DATA_BSBLAN_CLIENT: Final = "bsblan_client" +DATA_BSBLAN_TIMER: Final = "bsblan_timer" +DATA_BSBLAN_UPDATED: Final = "bsblan_updated" -ATTR_IDENTIFIERS = "identifiers" -ATTR_MODEL = "model" -ATTR_MANUFACTURER = "manufacturer" +ATTR_IDENTIFIERS: Final = "identifiers" +ATTR_MODEL: Final = "model" +ATTR_MANUFACTURER: Final = "manufacturer" -ATTR_TARGET_TEMPERATURE = "target_temperature" -ATTR_INSIDE_TEMPERATURE = "inside_temperature" -ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_TARGET_TEMPERATURE: Final = "target_temperature" +ATTR_INSIDE_TEMPERATURE: Final = "inside_temperature" +ATTR_OUTSIDE_TEMPERATURE: Final = "outside_temperature" -ATTR_STATE_ON = "on" -ATTR_STATE_OFF = "off" +ATTR_STATE_ON: Final = "on" +ATTR_STATE_OFF: Final = "off" -CONF_DEVICE_IDENT = "device_identification" -CONF_CONTROLLER_FAM = "controller_family" -CONF_CONTROLLER_VARI = "controller_variant" +CONF_DEVICE_IDENT: Final = "device_identification" +CONF_CONTROLLER_FAM: Final = "controller_family" +CONF_CONTROLLER_VARI: Final = "controller_variant" -SENSOR_TYPE_TEMPERATURE = "temperature" +SENSOR_TYPE_TEMPERATURE: Final = "temperature" -CONF_PASSKEY = "passkey" +CONF_PASSKEY: Final = "passkey" diff --git a/mypy.ini b/mypy.ini index c3c89a7b08d..90880107761 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1103,9 +1103,6 @@ ignore_errors = true [mypy-homeassistant.components.bmw_connected_drive.*] ignore_errors = true -[mypy-homeassistant.components.bsblan.*] -ignore_errors = true - [mypy-homeassistant.components.cert_expiry.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 407dec6849a..f47966bdb5c 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -26,7 +26,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.azure_event_hub.*", "homeassistant.components.blueprint.*", "homeassistant.components.bmw_connected_drive.*", - "homeassistant.components.bsblan.*", "homeassistant.components.cert_expiry.*", "homeassistant.components.climacell.*", "homeassistant.components.cloud.*", From 1b478ba02efa76ed88c55df089c437cead27b759 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 19 Jul 2021 19:39:32 +0200 Subject: [PATCH 383/818] Remove yaml support from Synology DSM (#53197) --- .../components/synology_dsm/__init__.py | 46 +------------ .../components/synology_dsm/config_flow.py | 6 -- .../synology_dsm/test_config_flow.py | 65 +------------------ 3 files changed, 3 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 84b392eb3fe..0bbf5febbc5 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -21,12 +21,10 @@ from synology_dsm.exceptions import ( SynologyDSMLoginFailedException, SynologyDSMRequestException, ) -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_DISKS, CONF_HOST, CONF_MAC, CONF_PASSWORD, @@ -46,7 +44,6 @@ from homeassistant.helpers.device_registry import ( async_get_registry as get_dev_reg, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -56,12 +53,10 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_DEVICE_TOKEN, CONF_SERIAL, - CONF_VOLUMES, COORDINATOR_CAMERAS, COORDINATOR_CENTRAL, COORDINATOR_SWITCHES, DEFAULT_SCAN_INTERVAL, - DEFAULT_USE_SSL, DEFAULT_VERIFY_SSL, DOMAIN, ENTITY_CLASS, @@ -83,26 +78,8 @@ from .const import ( EntityInfo, ) -CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_USE_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DISKS): cv.ensure_list, - vol.Optional(CONF_VOLUMES): cv.ensure_list, - } -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONFIG_SCHEMA]))}, - ), - extra=vol.ALLOW_EXTRA, -) ATTRIBUTION = "Data provided by Synology" @@ -110,25 +87,6 @@ ATTRIBUTION = "Data provided by Synology" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Synology DSM sensors from legacy config file.""" - - conf = config.get(DOMAIN) - if conf is None: - return True - - for dsm_conf in conf: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=dsm_conf, - ) - ) - - return True - - async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 1a3681daf32..5f11f158cec 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -227,12 +227,6 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Import a config entry.""" - return await self.async_step_user(user_input) - async def async_step_link(self, user_input: dict[str, Any]) -> FlowResult: """Link a config entry from discovery.""" return await self.async_step_user(user_input) diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 9c89ec64666..0eb9cb66852 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -23,7 +23,7 @@ from homeassistant.components.synology_dsm.const import ( DEFAULT_VERIFY_SSL, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -41,7 +41,6 @@ from homeassistant.core import HomeAssistant from .consts import ( DEVICE_TOKEN, HOST, - HOST_2, MACS, PASSWORD, PORT, @@ -256,59 +255,6 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): assert result["data"].get(CONF_VOLUMES) is None -async def test_import(hass: HomeAssistant, service: MagicMock): - """Test import step.""" - # import with minimum setup - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == SERIAL - assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == DEFAULT_PORT_SSL - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None - - service.return_value.information.serial = SERIAL_2 - # import with all - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: HOST_2, - CONF_PORT: PORT, - CONF_SSL: USE_SSL, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_DISKS: ["sda", "sdb", "sdc"], - CONF_VOLUMES: ["volume_1"], - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == SERIAL_2 - assert result["title"] == HOST_2 - assert result["data"][CONF_HOST] == HOST_2 - assert result["data"][CONF_PORT] == PORT - assert result["data"][CONF_SSL] == USE_SSL - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"][CONF_DISKS] == ["sda", "sdb", "sdc"] - assert result["data"][CONF_VOLUMES] == ["volume_1"] - - async def test_abort_if_already_setup(hass: HomeAssistant, service: MagicMock): """Test we abort if the account is already setup.""" MockConfigEntry( @@ -317,15 +263,6 @@ async def test_abort_if_already_setup(hass: HomeAssistant, service: MagicMock): unique_id=SERIAL, ).add_to_hass(hass) - # Should fail, same HOST:PORT (import) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - # Should fail, same HOST:PORT (flow) result = await hass.config_entries.flow.async_init( DOMAIN, From 8527179c0ebd9f84f49e7f911f6d0ae2fa7ebc09 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 19 Jul 2021 15:19:36 -0400 Subject: [PATCH 384/818] Use entity class attributes for bme280 (#53035) * Use entity class attributes for bme280 * add back device class oops * tweak --- homeassistant/components/bme280/sensor.py | 31 +++++------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 6a54432d190..d561d1fdddd 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -142,42 +142,25 @@ class BME280Sensor(SensorEntity): def __init__(self, bme280_client, sensor_type, temp_unit, name): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] + self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self.bme280_client = bme280_client self.temp_unit = temp_unit self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return self._unit_of_measurement - async def async_update(self): """Get the latest data from the BME280 and update the states.""" await self.hass.async_add_executor_job(self.bme280_client.update) if self.bme280_client.sensor.sample_ok: if self.type == SENSOR_TEMP: - temperature = round(self.bme280_client.sensor.temperature, 2) if self.temp_unit == TEMP_FAHRENHEIT: - temperature = round(celsius_to_fahrenheit(temperature), 2) - self._state = temperature + self._attr_state = round(celsius_to_fahrenheit(self.state), 2) + else: + self._attr_state = round(self.bme280_client.sensor.temperature, 2) elif self.type == SENSOR_HUMID: - self._state = round(self.bme280_client.sensor.humidity, 1) + self._attr_state = round(self.bme280_client.sensor.humidity, 1) elif self.type == SENSOR_PRESS: - self._state = round(self.bme280_client.sensor.pressure, 1) + self._attr_state = round(self.bme280_client.sensor.pressure, 1) else: _LOGGER.warning("Bad update of sensor.%s", self.name) From f5b3118d3c734187a25d58e986e4284a9a33f464 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 19 Jul 2021 15:22:20 -0400 Subject: [PATCH 385/818] Use entity class attributes for buienradar (#53166) --- homeassistant/components/buienradar/sensor.py | 142 +++++------------- .../components/buienradar/weather.py | 26 +--- 2 files changed, 46 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index e08b8072a18..7af84f48af7 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -356,32 +356,29 @@ async def async_setup_entry( class BrSensor(SensorEntity): """Representation of an Buienradar sensor.""" + _attr_entity_registry_enabled_default = False + _attr_should_poll = False + def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - self.client_name = client_name - self._name = SENSOR_TYPES[sensor_type][0] + self._attr_name = f"{client_name} {SENSOR_TYPES[sensor_type][0]}" + self._attr_icon = SENSOR_TYPES[sensor_type][2] self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._entity_picture = None - self._attribution = None + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._measured = None - self._stationname = None - self._unique_id = self.uid(coordinates) + self._attr_unique_id = "{:2.6f}{:2.6f}{}".format( + coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], sensor_type + ) + self._attr_device_class = SENSOR_TYPES[sensor_type][3] # All continuous sensors should be forced to be updated - self._force_update = self.type != SYMBOL and not self.type.startswith(CONDITION) - - if self.type.startswith(PRECIPITATION_FORECAST): - self._timeframe = None - - def uid(self, coordinates): - """Generate a unique id using coordinates and sensor type.""" - # The combination of the location, name and sensor type is unique - return "{:2.6f}{:2.6f}{}".format( - coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], self.type + self._attr_force_update = sensor_type != SYMBOL and not sensor_type.startswith( + CONDITION ) + if sensor_type.startswith(PRECIPITATION_FORECAST): + self._timeframe = None + @callback def data_updated(self, data): """Update data.""" @@ -398,8 +395,6 @@ class BrSensor(SensorEntity): if self._measured == data.get(MEASURED): return False - self._attribution = data.get(ATTRIBUTION) - self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) if ( @@ -442,18 +437,18 @@ class BrSensor(SensorEntity): img = condition.get(IMAGE) - if new_state != self._state or img != self._entity_picture: - self._state = new_state - self._entity_picture = img + if new_state != self.state or img != self.entity_picture: + self._attr_state = new_state + self._attr_entity_picture = img return True return False if self.type.startswith(WINDSPEED): # hass wants windspeeds in km/h not m/s, so convert: try: - self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) - if self._state is not None: - self._state = round(self._state * 3.6, 1) + self._attr_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + if self.state is not None: + self._attr_state = round(self.state * 3.6, 1) return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) @@ -461,7 +456,7 @@ class BrSensor(SensorEntity): # update all other sensors try: - self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._attr_state = data.get(FORECAST)[fcday].get(self.type[:-3]) return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) @@ -484,9 +479,9 @@ class BrSensor(SensorEntity): img = condition.get(IMAGE) - if new_state != self._state or img != self._entity_picture: - self._state = new_state - self._entity_picture = img + if new_state != self.state or img != self.entity_picture: + self._attr_state = new_state + self._attr_entity_picture = img return True return False @@ -495,99 +490,40 @@ class BrSensor(SensorEntity): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - self._state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) + self._attr_state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) return True if self.type in [WINDSPEED, WINDGUST]: # hass wants windspeeds in km/h not m/s, so convert: - self._state = data.get(self.type) - if self._state is not None: - self._state = round(data.get(self.type) * 3.6, 1) + self._attr_state = data.get(self.type) + if self.state is not None: + self._attr_state = round(data.get(self.type) * 3.6, 1) return True if self.type == VISIBILITY: # hass wants visibility in km (not m), so convert: - self._state = data.get(self.type) - if self._state is not None: - self._state = round(self._state / 1000, 1) + self._attr_state = data.get(self.type) + if self.state is not None: + self._attr_state = round(self.state / 1000, 1) return True # update all other sensors - self._state = data.get(self.type) - return True - - @property - def attribution(self): - """Return the attribution.""" - return self._attribution - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def entity_picture(self): - """Weather symbol if type is symbol.""" - return self._entity_picture - - @property - def extra_state_attributes(self): - """Return the state attributes.""" + self._attr_state = data.get(self.type) if self.type.startswith(PRECIPITATION_FORECAST): - result = {ATTR_ATTRIBUTION: self._attribution} + result = {ATTR_ATTRIBUTION: data.get(ATTRIBUTION)} if self._timeframe is not None: result[TIMEFRAME_LABEL] = "%d min" % (self._timeframe) - return result + self._attr_extra_state_attributes = result result = { - ATTR_ATTRIBUTION: self._attribution, - SENSOR_TYPES["stationname"][0]: self._stationname, + ATTR_ATTRIBUTION: data.get(ATTRIBUTION), + SENSOR_TYPES["stationname"][0]: data.get(STATIONNAME), } if self._measured is not None: # convert datetime (Europe/Amsterdam) into local datetime local_dt = dt_util.as_local(self._measured) result[MEASURED_LABEL] = local_dt.strftime("%c") - return result - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES[self.type][3] - - @property - def icon(self): - """Return possible sensor specific icon.""" - return SENSOR_TYPES[self.type][2] - - @property - def force_update(self): - """Return true for continuous sensors, false for discrete sensors.""" - return self._force_update - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False + self._attr_extra_state_attributes = result + return True diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 0c5080fc58b..a1546120064 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -111,12 +111,17 @@ async def async_setup_entry( class BrWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, data, config, coordinates): - """Initialise the platform with a data instance and station name.""" + """Initialize the platform with a data instance and station name.""" self._stationname = config.get(CONF_NAME, "Buienradar") + self._attr_name = ( + self._stationname or f"BR {data.stationname or '(unknown station)'}" + ) self._data = data - self._unique_id = "{:2.6f}{:2.6f}".format( + self._attr_unique_id = "{:2.6f}{:2.6f}".format( coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE] ) @@ -125,13 +130,6 @@ class BrWeather(WeatherEntity): """Return the attribution.""" return self._data.attribution - @property - def name(self): - """Return the name of the sensor.""" - return ( - self._stationname or f"BR {self._data.stationname or '(unknown station)'}" - ) - @property def condition(self): """Return the current condition.""" @@ -176,11 +174,6 @@ class BrWeather(WeatherEntity): """Return the current wind bearing (degrees).""" return self._data.wind_bearing - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def forecast(self): """Return the forecast array.""" @@ -207,8 +200,3 @@ class BrWeather(WeatherEntity): fcdata_out.append(data_out) return fcdata_out - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id From 450fdc91e47fe9cf76224db8141098e265ad0b79 Mon Sep 17 00:00:00 2001 From: RDFurman Date: Mon, 19 Jul 2021 13:44:02 -0600 Subject: [PATCH 386/818] Add honeywell config flow (#50731) * Upgrade honeywell from platform to integration * Add codeowner and run code formatter * Add sensors for current indoor temp and humidity * Fix tests and away temp * Spring cleaning of honeywell tests * Add config flow to honeywell integration * Add config flow test * Tie in honeywell service update * Simplify config flow and add import * Remove unnecessary platform schema * Clean up based on PR comments * Use new helper method * Force single device and fix linter errors * Address PR feedback * Update translations * Change string key and remove logger message * Always add first device * Fix test assertion * Put PLATFORM_SCHEMA back * Skip code coverage check on honeywell init * add some tests for honeywell * Make retry async * Make device private * Use _attr_ instead of properties * Code cleanup from PR feedback * Fix test and cleanup code * Make description better Co-authored-by: Matt Zimmerman --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/honeywell/__init__.py | 131 ++++++ homeassistant/components/honeywell/climate.py | 218 +++------ .../components/honeywell/config_flow.py | 55 +++ homeassistant/components/honeywell/const.py | 13 + .../components/honeywell/manifest.json | 3 +- .../components/honeywell/strings.json | 17 + .../components/honeywell/translations/en.json | 17 + homeassistant/generated/config_flows.py | 1 + tests/components/honeywell/conftest.py | 65 +++ tests/components/honeywell/test_climate.py | 430 ------------------ .../components/honeywell/test_config_flow.py | 63 +++ tests/components/honeywell/test_init.py | 8 + 14 files changed, 442 insertions(+), 581 deletions(-) create mode 100644 homeassistant/components/honeywell/config_flow.py create mode 100644 homeassistant/components/honeywell/const.py create mode 100644 homeassistant/components/honeywell/strings.json create mode 100644 homeassistant/components/honeywell/translations/en.json create mode 100644 tests/components/honeywell/conftest.py delete mode 100644 tests/components/honeywell/test_climate.py create mode 100644 tests/components/honeywell/test_config_flow.py create mode 100644 tests/components/honeywell/test_init.py diff --git a/.coveragerc b/.coveragerc index 6d7363c3d04..2f60e74480f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -435,6 +435,7 @@ omit = homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py homeassistant/components/homeworks/* + homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c269ad64888..acd2b6d44c3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -214,6 +214,7 @@ homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco homeassistant/components/homekit_controller/* @Jc2k @bdraco homeassistant/components/homematic/* @pvizeli @danielperna84 +homeassistant/components/honeywell/* @rdfurman homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 57176c9acf8..48f2802e89f 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -1 +1,132 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" +from datetime import timedelta + +import somecomfort + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.util import Throttle + +from .const import _LOGGER, CONF_DEV_ID, CONF_LOC_ID, DOMAIN + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) +PLATFORMS = ["climate"] + + +async def async_setup_entry(hass, config): + """Set up the Honeywell thermostat.""" + username = config.data[CONF_USERNAME] + password = config.data[CONF_PASSWORD] + + client = await hass.async_add_executor_job( + get_somecomfort_client, username, password + ) + + if client is None: + return False + + loc_id = config.data.get(CONF_LOC_ID) + dev_id = config.data.get(CONF_DEV_ID) + + devices = [] + + for location in client.locations_by_id.values(): + for device in location.devices_by_id.values(): + if (not loc_id or location.locationid == loc_id) and ( + not dev_id or device.deviceid == dev_id + ): + devices.append(device) + + if len(devices) == 0: + _LOGGER.debug("No devices found") + return False + + data = HoneywellService(hass, client, username, password, devices[0]) + await data.update() + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config.entry_id] = data + hass.config_entries.async_setup_platforms(config, PLATFORMS) + + return True + + +def get_somecomfort_client(username, password): + """Initialize the somecomfort client.""" + try: + return somecomfort.SomeComfort(username, password) + except somecomfort.AuthError: + _LOGGER.error("Failed to login to honeywell account %s", username) + return None + except somecomfort.SomeComfortError as ex: + raise ConfigEntryNotReady( + "Failed to initialize the Honeywell client: " + "Check your configuration (username, password), " + "or maybe you have exceeded the API rate limit?" + ) from ex + + +class HoneywellService: + """Get the latest data and update.""" + + def __init__(self, hass, client, username, password, device): + """Initialize the data object.""" + self._hass = hass + self._client = client + self._username = username + self._password = password + self.device = device + + async def _retry(self) -> bool: + """Recreate a new somecomfort client. + + When we got an error, the best way to be sure that the next query + will succeed, is to recreate a new somecomfort client. + """ + self._client = await self._hass.async_add_executor_job( + get_somecomfort_client, self._username, self._password + ) + + if self._client is None: + return False + + devices = [ + device + for location in self._client.locations_by_id.values() + for device in location.devices_by_id.values() + if device.name == self.device.name + ] + + if len(devices) != 1: + _LOGGER.error("Failed to find device %s", self.device.name) + return False + + self.device = devices[0] + return True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def update(self) -> None: + """Update the state.""" + retries = 3 + while retries > 0: + try: + await self._hass.async_add_executor_job(self.device.refresh) + break + except ( + somecomfort.client.APIRateLimited, + OSError, + somecomfort.client.ConnectionTimeout, + ) as exp: + retries -= 1 + if retries == 0: + raise exp + + result = await self._hass.async_add_executor_job(self._retry()) + + if not result: + raise exp + + _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) + + _LOGGER.debug( + "latestData = %s ", self.device._data # pylint: disable=protected-access + ) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 8053ad85502..36fe16aeaa2 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -2,10 +2,8 @@ from __future__ import annotations import datetime -import logging from typing import Any -import requests import somecomfort import voluptuous as vol @@ -33,6 +31,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_TEMPERATURE, CONF_PASSWORD, @@ -42,19 +41,21 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr -_LOGGER = logging.getLogger(__name__) +from .const import ( + _LOGGER, + CONF_COOL_AWAY_TEMPERATURE, + CONF_DEV_ID, + CONF_HEAT_AWAY_TEMPERATURE, + CONF_LOC_ID, + DEFAULT_COOL_AWAY_TEMPERATURE, + DEFAULT_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) ATTR_FAN_ACTION = "fan_action" -CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature" -CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature" -CONF_DEV_ID = "thermostat" -CONF_LOC_ID = "location" - -DEFAULT_COOL_AWAY_TEMPERATURE = 88 -DEFAULT_HEAT_AWAY_TEMPERATURE = 61 - ATTR_PERMANENT_HOLD = "permanent_hold" PLATFORM_SCHEMA = vol.All( @@ -108,95 +109,88 @@ HW_FAN_MODE_TO_HA = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): """Set up the Honeywell thermostat.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) + cool_away_temp = config.data.get(CONF_COOL_AWAY_TEMPERATURE) + heat_away_temp = config.data.get(CONF_HEAT_AWAY_TEMPERATURE) - try: - client = somecomfort.SomeComfort(username, password) - except somecomfort.AuthError: - _LOGGER.error("Failed to login to honeywell account %s", username) - return - except somecomfort.SomeComfortError: - _LOGGER.error( - "Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - "or maybe you have exceeded the API rate limit?" + data = hass.data[DOMAIN][config.entry_id] + + async_add_entities([HoneywellUSThermostat(data, cool_away_temp, heat_away_temp)]) + + +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Honeywell climate platform. + + Honeywell uses config flow for configuration now. If an entry exists in + configuration.yaml, the import flow will attempt to import it and create + a config entry. + """ + + if config["platform"] == "honeywell": + _LOGGER.warning( + "Loading honeywell via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed" ) - return - - dev_id = config.get(CONF_DEV_ID) - loc_id = config.get(CONF_LOC_ID) - cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE) - heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE) - - add_entities( - [ - HoneywellUSThermostat( - client, - device, - cool_away_temp, - heat_away_temp, - username, - password, + # No config entry exists and configuration.yaml config exists, trigger the import flow. + if not hass.config_entries.async_entries(DOMAIN): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - for location in client.locations_by_id.values() - for device in location.devices_by_id.values() - if ( - (not loc_id or location.locationid == loc_id) - and (not dev_id or device.deviceid == dev_id) - ) - ] - ) class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" - def __init__( - self, client, device, cool_away_temp, heat_away_temp, username, password - ): + def __init__(self, data, cool_away_temp, heat_away_temp): """Initialize the thermostat.""" - self._client = client - self._device = device + self._data = data self._cool_away_temp = cool_away_temp self._heat_away_temp = heat_away_temp self._away = False - self._username = username - self._password = password - _LOGGER.debug("latestData = %s ", device._data) + self._attr_unique_id = dr.format_mac(data.device.mac_address) + self._attr_name = data.device.name + self._attr_temperature_unit = ( + TEMP_CELSIUS if data.device.temperature_unit == "C" else TEMP_FAHRENHEIT + ) + self._attr_preset_modes = [PRESET_NONE, PRESET_AWAY] + self._attr_is_aux_heat = data.device.system_mode == "emheat" # not all honeywell HVACs support all modes - mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] + mappings = [ + v for k, v in HVAC_MODE_TO_HW_MODE.items() if data.device.raw_ui_data[k] + ] self._hvac_mode_map = {k: v for d in mappings for k, v in d.items()} + self._attr_hvac_modes = list(self._hvac_mode_map) - self._supported_features = ( + self._attr_supported_features = ( SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE ) - if device._data["canControlHumidification"]: - self._supported_features |= SUPPORT_TARGET_HUMIDITY + if data.device._data["canControlHumidification"]: + self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY - if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: - self._supported_features |= SUPPORT_AUX_HEAT + if data.device.raw_ui_data["SwitchEmergencyHeatAllowed"]: + self._attr_supported_features |= SUPPORT_AUX_HEAT - if not device._data["hasFan"]: + if not data.device._data["hasFan"]: return # not all honeywell fans support all modes - mappings = [v for k, v in FAN_MODE_TO_HW.items() if device.raw_fan_data[k]] + mappings = [v for k, v in FAN_MODE_TO_HW.items() if data.device.raw_fan_data[k]] self._fan_mode_map = {k: v for d in mappings for k, v in d.items()} - self._supported_features |= SUPPORT_FAN_MODE + self._attr_fan_modes = list(self._fan_mode_map) + + self._attr_supported_features |= SUPPORT_FAN_MODE @property - def name(self) -> str | None: - """Return the name of the honeywell, if any.""" - return self._device.name + def _device(self): + """Shortcut to access the device.""" + return self._data.device @property def extra_state_attributes(self) -> dict[str, Any]: @@ -208,11 +202,6 @@ class HoneywellUSThermostat(ClimateEntity): data["dr_phase"] = self._device.raw_dr_data.get("Phase") return data - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return self._supported_features - @property def min_temp(self) -> float: """Return the minimum temperature.""" @@ -231,11 +220,6 @@ class HoneywellUSThermostat(ClimateEntity): return self._device.raw_ui_data["HeatUpperSetptLimit"] return None - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS if self._device.temperature_unit == "C" else TEMP_FAHRENHEIT - @property def current_humidity(self) -> int | None: """Return the current humidity.""" @@ -246,11 +230,6 @@ class HoneywellUSThermostat(ClimateEntity): """Return hvac operation ie. heat, cool mode.""" return HW_MODE_TO_HVAC_MODE[self._device.system_mode] - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return list(self._hvac_mode_map) - @property def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" @@ -291,26 +270,11 @@ class HoneywellUSThermostat(ClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return PRESET_AWAY if self._away else None - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return [PRESET_NONE, PRESET_AWAY] - - @property - def is_aux_heat(self) -> str | None: - """Return true if aux heater.""" - return self._device.system_mode == "emheat" - @property def fan_mode(self) -> str | None: """Return the fan setting.""" return HW_FAN_MODE_TO_HA[self._device.fan_mode] - @property - def fan_modes(self) -> list[str] | None: - """Return the list of available fan modes.""" - return list(self._fan_mode_map) - def _is_permanent_hold(self) -> bool: heat_status = self._device.raw_ui_data.get("StatusHeat", 0) cool_status = self._device.raw_ui_data.get("StatusCool", 0) @@ -383,7 +347,9 @@ class HoneywellUSThermostat(ClimateEntity): setattr(self._device, f"hold_{mode}", True) # Set temperature setattr( - self._device, f"setpoint_{mode}", getattr(self, f"_{mode}_away_temp") + self._device, + f"setpoint_{mode}", + getattr(self, f"_{mode}_away_temp"), ) except somecomfort.SomeComfortError: _LOGGER.error( @@ -418,54 +384,6 @@ class HoneywellUSThermostat(ClimateEntity): else: self.set_hvac_mode(HVAC_MODE_OFF) - def _retry(self) -> bool: - """Recreate a new somecomfort client. - - When we got an error, the best way to be sure that the next query - will succeed, is to recreate a new somecomfort client. - """ - try: - self._client = somecomfort.SomeComfort(self._username, self._password) - except somecomfort.AuthError: - _LOGGER.error("Failed to login to honeywell account %s", self._username) - return False - except somecomfort.SomeComfortError as ex: - _LOGGER.error("Failed to initialize honeywell client: %s", str(ex)) - return False - - devices = [ - device - for location in self._client.locations_by_id.values() - for device in location.devices_by_id.values() - if device.name == self._device.name - ] - - if len(devices) != 1: - _LOGGER.error("Failed to find device %s", self._device.name) - return False - - self._device = devices[0] - return True - - def update(self) -> None: - """Update the state.""" - retries = 3 - while retries > 0: - try: - self._device.refresh() - break - except ( - somecomfort.client.APIRateLimited, - OSError, - requests.exceptions.ReadTimeout, - ) as exp: - retries -= 1 - if retries == 0: - raise exp - if not self._retry(): - raise exp - _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) - - _LOGGER.debug( - "latestData = %s ", self._device._data # pylint: disable=protected-access - ) + async def async_update(self): + """Get the latest state from the service.""" + await self._data.update() diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py new file mode 100644 index 00000000000..318809aaa03 --- /dev/null +++ b/homeassistant/components/honeywell/config_flow.py @@ -0,0 +1,55 @@ +"""Config flow to configure the honeywell integration.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.honeywell import get_somecomfort_client +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, DOMAIN + + +class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a honeywell config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Create config entry. Show the setup form to the user.""" + errors = {} + + if user_input is not None: + valid = await self.is_valid(**user_input) + if valid: + return self.async_create_entry( + title=DOMAIN, + data=user_input, + ) + + errors["base"] = "invalid_auth" + + data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + return self.async_show_form( + step_id="user", data_schema=vol.Schema(data_schema), errors=errors + ) + + async def is_valid(self, **kwargs) -> bool: + """Check if login credentials are valid.""" + client = await self.hass.async_add_executor_job( + get_somecomfort_client, kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD] + ) + + return client is not None + + async def async_step_import(self, import_data): + """Import entry from configuration.yaml.""" + return await self.async_step_user( + { + CONF_USERNAME: import_data[CONF_USERNAME], + CONF_PASSWORD: import_data[CONF_PASSWORD], + CONF_COOL_AWAY_TEMPERATURE: import_data[CONF_COOL_AWAY_TEMPERATURE], + CONF_HEAT_AWAY_TEMPERATURE: import_data[CONF_HEAT_AWAY_TEMPERATURE], + } + ) diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py new file mode 100644 index 00000000000..6102f30d3de --- /dev/null +++ b/homeassistant/components/honeywell/const.py @@ -0,0 +1,13 @@ +"""Support for Honeywell (US) Total Connect Comfort climate systems.""" +import logging + +DOMAIN = "honeywell" + +DEFAULT_COOL_AWAY_TEMPERATURE = 88 +DEFAULT_HEAT_AWAY_TEMPERATURE = 61 +CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature" +CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature" +CONF_DEV_ID = "thermostat" +CONF_LOC_ID = "location" + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index bd0c5dfca6d..5ba4947e046 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -1,8 +1,9 @@ { "domain": "honeywell", "name": "Honeywell Total Connect Comfort (US)", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": ["somecomfort==0.5.2"], - "codeowners": [], + "codeowners": ["@rdfurman"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json new file mode 100644 index 00000000000..ce76b571996 --- /dev/null +++ b/homeassistant/components/honeywell/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "title": "Honeywell Total Connect Comfort (US)", + "description": "Please enter the credentials used to log into mytotalconnectcomfort.com.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} diff --git a/homeassistant/components/honeywell/translations/en.json b/homeassistant/components/honeywell/translations/en.json new file mode 100644 index 00000000000..454093c5b3e --- /dev/null +++ b/homeassistant/components/honeywell/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please enter the credentials used to log into mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (US)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b04d4f50dd8..40b5f1360ed 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -113,6 +113,7 @@ FLOWS = [ "homekit", "homekit_controller", "homematicip_cloud", + "honeywell", "huawei_lte", "hue", "huisbaasje", diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py new file mode 100644 index 00000000000..05e3631e08d --- /dev/null +++ b/tests/components/honeywell/conftest.py @@ -0,0 +1,65 @@ +"""Fixtures for honeywell tests.""" + +from unittest.mock import create_autospec, patch + +import pytest +import somecomfort + +from homeassistant.components.honeywell.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_data(): + """Provide configuration data for tests.""" + return {CONF_USERNAME: "fake", CONF_PASSWORD: "user"} + + +@pytest.fixture +def config_entry(config_data): + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=config_data, + options={}, + ) + + +@pytest.fixture +def device(): + """Mock a somecomfort.Device.""" + mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device.deviceid.return_value = "device1" + mock_device._data = { + "canControlHumidification": False, + "hasFan": False, + } + mock_device.system_mode = "off" + mock_device.name = "device1" + mock_device.current_temperature = 20 + mock_device.mac_address = "macaddress1" + return mock_device + + +@pytest.fixture +def location(device): + """Mock a somecomfort.Location.""" + mock_location = create_autospec(somecomfort.Location, instance=True) + mock_location.locationid.return_value = "location1" + mock_location.devices_by_id = {device.deviceid: device} + return mock_location + + +@pytest.fixture(autouse=True) +def client(location): + """Mock a somecomfort.SomeComfort client.""" + client_mock = create_autospec(somecomfort.SomeComfort, instance=True) + client_mock.locations_by_id = {location.locationid: location} + + with patch( + "homeassistant.components.honeywell.somecomfort.SomeComfort" + ) as sc_class_mock: + sc_class_mock.return_value = client_mock + yield client_mock diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py deleted file mode 100644 index d97bbc86ed6..00000000000 --- a/tests/components/honeywell/test_climate.py +++ /dev/null @@ -1,430 +0,0 @@ -"""The test the Honeywell thermostat module.""" -import unittest -from unittest import mock - -import pytest -import requests.exceptions -import somecomfort -import voluptuous as vol - -from homeassistant.components.climate.const import ( - ATTR_FAN_MODE, - ATTR_FAN_MODES, - ATTR_HVAC_MODES, -) -import homeassistant.components.honeywell.climate as honeywell -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) - -pytestmark = pytest.mark.skip("Need to be fixed!") - - -class TestHoneywell(unittest.TestCase): - """A test class for Honeywell themostats.""" - - @mock.patch("somecomfort.SomeComfort") - @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") - def test_setup_us(self, mock_ht, mock_sc): - """Test for the US setup.""" - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "us", - } - bad_pass_config = {CONF_USERNAME: "user", honeywell.CONF_REGION: "us"} - bad_region_config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "un", - } - - with pytest.raises(vol.Invalid): - honeywell.PLATFORM_SCHEMA(None) - - with pytest.raises(vol.Invalid): - honeywell.PLATFORM_SCHEMA({}) - - with pytest.raises(vol.Invalid): - honeywell.PLATFORM_SCHEMA(bad_pass_config) - - with pytest.raises(vol.Invalid): - honeywell.PLATFORM_SCHEMA(bad_region_config) - - hass = mock.MagicMock() - add_entities = mock.MagicMock() - - locations = [mock.MagicMock(), mock.MagicMock()] - devices_1 = [mock.MagicMock()] - devices_2 = [mock.MagicMock(), mock.MagicMock] - mock_sc.return_value.locations_by_id.values.return_value = locations - locations[0].devices_by_id.values.return_value = devices_1 - locations[1].devices_by_id.values.return_value = devices_2 - - result = honeywell.setup_platform(hass, config, add_entities) - assert result - assert mock_sc.call_count == 1 - assert mock_sc.call_args == mock.call("user", "pass") - mock_ht.assert_has_calls( - [ - mock.call(mock_sc.return_value, devices_1[0], 18, 28, "user", "pass"), - mock.call(mock_sc.return_value, devices_2[0], 18, 28, "user", "pass"), - mock.call(mock_sc.return_value, devices_2[1], 18, 28, "user", "pass"), - ] - ) - - @mock.patch("somecomfort.SomeComfort") - def test_setup_us_failures(self, mock_sc): - """Test the US setup.""" - hass = mock.MagicMock() - add_entities = mock.MagicMock() - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "us", - } - - mock_sc.side_effect = somecomfort.AuthError - result = honeywell.setup_platform(hass, config, add_entities) - assert not result - assert not add_entities.called - - mock_sc.side_effect = somecomfort.SomeComfortError - result = honeywell.setup_platform(hass, config, add_entities) - assert not result - assert not add_entities.called - - @mock.patch("somecomfort.SomeComfort") - @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") - def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None): - """Test for US filtered thermostats.""" - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "us", - "location": loc, - "thermostat": dev, - } - locations = { - 1: mock.MagicMock( - locationid=mock.sentinel.loc1, - devices_by_id={ - 11: mock.MagicMock(deviceid=mock.sentinel.loc1dev1), - 12: mock.MagicMock(deviceid=mock.sentinel.loc1dev2), - }, - ), - 2: mock.MagicMock( - locationid=mock.sentinel.loc2, - devices_by_id={21: mock.MagicMock(deviceid=mock.sentinel.loc2dev1)}, - ), - 3: mock.MagicMock( - locationid=mock.sentinel.loc3, - devices_by_id={31: mock.MagicMock(deviceid=mock.sentinel.loc3dev1)}, - ), - } - mock_sc.return_value = mock.MagicMock(locations_by_id=locations) - hass = mock.MagicMock() - add_entities = mock.MagicMock() - assert honeywell.setup_platform(hass, config, add_entities) is True - - return mock_ht.call_args_list, mock_sc - - def test_us_filtered_thermostat_1(self): - """Test for US filtered thermostats.""" - result, client = self._test_us_filtered_devices(dev=mock.sentinel.loc1dev1) - devices = [x[0][1].deviceid for x in result] - assert [mock.sentinel.loc1dev1] == devices - - def test_us_filtered_thermostat_2(self): - """Test for US filtered location.""" - result, client = self._test_us_filtered_devices(dev=mock.sentinel.loc2dev1) - devices = [x[0][1].deviceid for x in result] - assert [mock.sentinel.loc2dev1] == devices - - def test_us_filtered_location_1(self): - """Test for US filtered locations.""" - result, client = self._test_us_filtered_devices(loc=mock.sentinel.loc1) - devices = [x[0][1].deviceid for x in result] - assert [mock.sentinel.loc1dev1, mock.sentinel.loc1dev2] == devices - - def test_us_filtered_location_2(self): - """Test for US filtered locations.""" - result, client = self._test_us_filtered_devices(loc=mock.sentinel.loc2) - devices = [x[0][1].deviceid for x in result] - assert [mock.sentinel.loc2dev1] == devices - - @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") - def test_eu_setup_full_config(self, mock_round, mock_evo): - """Test the EU setup with complete configuration.""" - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "eu", - } - mock_evo.return_value.temperatures.return_value = [{"id": "foo"}, {"id": "bar"}] - hass = mock.MagicMock() - add_entities = mock.MagicMock() - assert honeywell.setup_platform(hass, config, add_entities) - assert mock_evo.call_count == 1 - assert mock_evo.call_args == mock.call("user", "pass") - assert mock_evo.return_value.temperatures.call_count == 1 - assert mock_evo.return_value.temperatures.call_args == mock.call( - force_refresh=True - ) - mock_round.assert_has_calls( - [ - mock.call(mock_evo.return_value, "foo", True, 20.0), - mock.call(mock_evo.return_value, "bar", False, 20.0), - ] - ) - assert add_entities.call_count == 2 - - @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") - def test_eu_setup_partial_config(self, mock_round, mock_evo): - """Test the EU setup with partial configuration.""" - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "eu", - } - - mock_evo.return_value.temperatures.return_value = [{"id": "foo"}, {"id": "bar"}] - - hass = mock.MagicMock() - add_entities = mock.MagicMock() - assert honeywell.setup_platform(hass, config, add_entities) - mock_round.assert_has_calls( - [ - mock.call(mock_evo.return_value, "foo", True, 16), - mock.call(mock_evo.return_value, "bar", False, 16), - ] - ) - - @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") - def test_eu_setup_bad_temp(self, mock_round, mock_evo): - """Test the EU setup with invalid temperature.""" - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "eu", - } - - with pytest.raises(vol.Invalid): - honeywell.PLATFORM_SCHEMA(config) - - @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") - def test_eu_setup_error(self, mock_round, mock_evo): - """Test the EU setup with errors.""" - config = { - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - honeywell.CONF_REGION: "eu", - } - mock_evo.return_value.temperatures.side_effect = ( - requests.exceptions.RequestException - ) - add_entities = mock.MagicMock() - hass = mock.MagicMock() - assert not honeywell.setup_platform(hass, config, add_entities) - - -class TestHoneywellRound(unittest.TestCase): - """A test class for Honeywell Round thermostats.""" - - def setup_method(self, method): - """Test the setup method.""" - - def fake_temperatures(force_refresh=None): - """Create fake temperatures.""" - temps = [ - { - "id": "1", - "temp": 20, - "setpoint": 21, - "thermostat": "main", - "name": "House", - }, - { - "id": "2", - "temp": 21, - "setpoint": 22, - "thermostat": "DOMESTIC_HOT_WATER", - }, - ] - return temps - - self.device = mock.MagicMock() - self.device.temperatures.side_effect = fake_temperatures - self.round1 = honeywell.RoundThermostat(self.device, "1", True, 16) - self.round1.update() - self.round2 = honeywell.RoundThermostat(self.device, "2", False, 17) - self.round2.update() - - def test_attributes(self): - """Test the attributes.""" - assert self.round1.name == "House" - assert self.round1.temperature_unit == TEMP_CELSIUS - assert self.round1.current_temperature == 20 - assert self.round1.target_temperature == 21 - assert not self.round1.is_away_mode_on - - assert self.round2.name == "Hot Water" - assert self.round2.temperature_unit == TEMP_CELSIUS - assert self.round2.current_temperature == 21 - assert self.round2.target_temperature is None - assert not self.round2.is_away_mode_on - - def test_away_mode(self): - """Test setting the away mode.""" - assert not self.round1.is_away_mode_on - self.round1.turn_away_mode_on() - assert self.round1.is_away_mode_on - assert self.device.set_temperature.call_count == 1 - assert self.device.set_temperature.call_args == mock.call("House", 16) - - self.device.set_temperature.reset_mock() - self.round1.turn_away_mode_off() - assert not self.round1.is_away_mode_on - assert self.device.cancel_temp_override.call_count == 1 - assert self.device.cancel_temp_override.call_args == mock.call("House") - - def test_set_temperature(self): - """Test setting the temperature.""" - self.round1.set_temperature(temperature=25) - assert self.device.set_temperature.call_count == 1 - assert self.device.set_temperature.call_args == mock.call("House", 25) - - def test_set_hvac_mode(self) -> None: - """Test setting the system operation.""" - self.round1.set_hvac_mode("cool") - assert self.round1.current_operation == "cool" - assert self.device.system_mode == "cool" - - self.round1.set_hvac_mode("heat") - assert self.round1.current_operation == "heat" - assert self.device.system_mode == "heat" - - -class TestHoneywellUS(unittest.TestCase): - """A test class for Honeywell US thermostats.""" - - def setup_method(self, method): - """Test the setup method.""" - self.client = mock.MagicMock() - self.device = mock.MagicMock() - self.cool_away_temp = 18 - self.heat_away_temp = 28 - self.honeywell = honeywell.HoneywellUSThermostat( - self.client, - self.device, - self.cool_away_temp, - self.heat_away_temp, - "user", - "password", - ) - - self.device.fan_running = True - self.device.name = "test" - self.device.temperature_unit = "F" - self.device.current_temperature = 72 - self.device.setpoint_cool = 78 - self.device.setpoint_heat = 65 - self.device.system_mode = "heat" - self.device.fan_mode = "auto" - - def test_properties(self): - """Test the properties.""" - assert self.honeywell.is_fan_on - assert self.honeywell.name == "test" - assert self.honeywell.current_temperature == 72 - - def test_unit_of_measurement(self): - """Test the unit of measurement.""" - assert self.honeywell.temperature_unit == TEMP_FAHRENHEIT - self.device.temperature_unit = "C" - assert self.honeywell.temperature_unit == TEMP_CELSIUS - - def test_target_temp(self): - """Test the target temperature.""" - assert self.honeywell.target_temperature == 65 - self.device.system_mode = "cool" - assert self.honeywell.target_temperature == 78 - - def test_set_temp(self): - """Test setting the temperature.""" - self.honeywell.set_temperature(temperature=70) - assert self.device.setpoint_heat == 70 - assert self.honeywell.target_temperature == 70 - - self.device.system_mode = "cool" - assert self.honeywell.target_temperature == 78 - self.honeywell.set_temperature(temperature=74) - assert self.device.setpoint_cool == 74 - assert self.honeywell.target_temperature == 74 - - def test_set_hvac_mode(self) -> None: - """Test setting the operation mode.""" - self.honeywell.set_hvac_mode("cool") - assert self.device.system_mode == "cool" - - self.honeywell.set_hvac_mode("heat") - assert self.device.system_mode == "heat" - - def test_set_temp_fail(self): - """Test if setting the temperature fails.""" - self.device.setpoint_heat = mock.MagicMock( - side_effect=somecomfort.SomeComfortError - ) - self.honeywell.set_temperature(temperature=123) - - def test_attributes(self): - """Test the attributes.""" - expected = { - honeywell.ATTR_FAN: "running", - ATTR_FAN_MODE: "auto", - ATTR_FAN_MODES: somecomfort.FAN_MODES, - ATTR_HVAC_MODES: somecomfort.SYSTEM_MODES, - } - assert expected == self.honeywell.extra_state_attributes - expected["fan"] = "idle" - self.device.fan_running = False - assert self.honeywell.extra_state_attributes == expected - - def test_with_no_fan(self): - """Test if there is on fan.""" - self.device.fan_running = False - self.device.fan_mode = None - expected = { - honeywell.ATTR_FAN: "idle", - ATTR_FAN_MODE: None, - ATTR_FAN_MODES: somecomfort.FAN_MODES, - ATTR_HVAC_MODES: somecomfort.SYSTEM_MODES, - } - assert self.honeywell.extra_state_attributes == expected - - def test_heat_away_mode(self): - """Test setting the heat away mode.""" - self.honeywell.set_hvac_mode("heat") - assert not self.honeywell.is_away_mode_on - self.honeywell.turn_away_mode_on() - assert self.honeywell.is_away_mode_on - assert self.device.setpoint_heat == self.heat_away_temp - assert self.device.hold_heat is True - - self.honeywell.turn_away_mode_off() - assert not self.honeywell.is_away_mode_on - assert self.device.hold_heat is False - - @mock.patch("somecomfort.SomeComfort") - def test_retry(self, test_somecomfort): - """Test retry connection.""" - old_device = self.honeywell._device - self.honeywell._retry() - assert self.honeywell._device == old_device diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py new file mode 100644 index 00000000000..65f47ddf35f --- /dev/null +++ b/tests/components/honeywell/test_config_flow.py @@ -0,0 +1,63 @@ +"""Tests for honeywell config flow.""" +from unittest.mock import patch + +import somecomfort + +from homeassistant import data_entry_flow +from homeassistant.components.honeywell.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant + +FAKE_CONFIG = { + "username": "fake", + "password": "user", + "away_cool_temperature": 88, + "away_heat_temperature": 61, +} + + +async def test_show_authenticate_form(hass: HomeAssistant) -> None: + """Test that the config form is shown.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_connection_error(hass: HomeAssistant) -> None: + """Test that an error message is shown on login fail.""" + with patch( + "somecomfort.SomeComfort", + side_effect=somecomfort.AuthError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_create_entry(hass: HomeAssistant) -> None: + """Test that the config entry is created.""" + with patch( + "somecomfort.SomeComfort", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + + +async def test_async_step_import(hass: HomeAssistant) -> None: + """Test that the import step works.""" + with patch( + "somecomfort.SomeComfort", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py new file mode 100644 index 00000000000..d0bdb5ccf2d --- /dev/null +++ b/tests/components/honeywell/test_init.py @@ -0,0 +1,8 @@ +"""Test honeywell setup process.""" + + +async def test_setup_entry(hass, config_entry): + """Initialize the config entry.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() From ac9e4cb2f20f04ea60c36efa94d7f130d4c1df55 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 20 Jul 2021 00:11:11 +0000 Subject: [PATCH 387/818] [ci skip] Translation update --- .../components/coinbase/translations/ca.json | 2 +- .../components/coinbase/translations/nl.json | 1 + .../components/switcher_kis/translations/ca.json | 13 +++++++++++++ .../components/switcher_kis/translations/ru.json | 13 +++++++++++++ .../switcher_kis/translations/zh-Hant.json | 13 +++++++++++++ .../components/zwave_js/translations/nl.json | 8 ++++++++ 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switcher_kis/translations/ca.json create mode 100644 homeassistant/components/switcher_kis/translations/ru.json create mode 100644 homeassistant/components/switcher_kis/translations/zh-Hant.json diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json index a7990703ef0..ca0214372fb 100644 --- a/homeassistant/components/coinbase/translations/ca.json +++ b/homeassistant/components/coinbase/translations/ca.json @@ -31,7 +31,7 @@ "init": { "data": { "account_balance_currencies": "Saldos de cartera a informar.", - "exchange_base": "Moneda base per als sensors de tipus de canvi.", + "exchange_base": "Moneda base per als sensors de canvi de tipus.", "exchange_rate_currencies": "Tipus de canvi a informar." }, "description": "Ajusta les opcions de Coinbase" diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index 052caf2f358..b13caef1976 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Wallet-saldi om te rapporteren.", + "exchange_base": "Basisvaluta voor wisselkoerssensoren.", "exchange_rate_currencies": "Wisselkoersen om te rapporteren." }, "description": "Coinbase-opties aanpassen" diff --git a/homeassistant/components/switcher_kis/translations/ca.json b/homeassistant/components/switcher_kis/translations/ca.json new file mode 100644 index 00000000000..dc21c371e60 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/ru.json b/homeassistant/components/switcher_kis/translations/ru.json new file mode 100644 index 00000000000..85a42bf1be5 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/zh-Hant.json b/homeassistant/components/switcher_kis/translations/zh-Hant.json new file mode 100644 index 00000000000..90c98e491df --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index d0475e20a3f..3696380b43a 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -56,6 +56,14 @@ "config_parameter": "Config parameter {subtype} waarde", "node_status": "Knooppuntstatus", "value": "Huidige waarde van een Z-Wave-waarde" + }, + "trigger_type": { + "event.notification.entry_control": "Stuur een Entry Control melding", + "event.notification.notification": "Stuur een notificatie", + "event.value_notification.basic": "Basis CC-evenement op {subtype}", + "event.value_notification.central_scene": "Centrale Sc\u00e8ne actie op {subtype}", + "event.value_notification.scene_activation": "Sc\u00e8ne-activering op {subtype}", + "state.node_status": "Knooppuntstatus gewijzigd" } }, "options": { From 7711ac901ce46ce2861a240df1d3297312874e8b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Jul 2021 05:22:13 +0200 Subject: [PATCH 388/818] Fix issues after pylint update (#53205) --- homeassistant/components/xiaomi_miio/fan.py | 4 +--- homeassistant/components/xiaomi_miio/switch.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 49ae1cf7b7c..c1dde7ec38d 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -635,9 +635,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await asyncio.wait(update_tasks) for air_purifier_service, method in SERVICE_TO_METHOD.items(): - schema = method[air_purifier_service].get( - "schema", AIRPURIFIER_SERVICE_SCHEMA - ) + schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, air_purifier_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 52682a05f08..5802ae2a00d 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -251,7 +251,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await asyncio.wait(update_tasks) for plug_service, method in SERVICE_TO_METHOD.items(): - schema = method[plug_service].get("schema", SERVICE_SCHEMA) + schema = method.get("schema", SERVICE_SCHEMA) hass.services.async_register( DOMAIN, plug_service, async_service_handler, schema=schema ) From 562aa74c77dd6b9e3f9ffcf44d8758e4399120e6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 20 Jul 2021 00:22:41 -0400 Subject: [PATCH 389/818] Switch to dataclass from dictionary for climacell sensor definitions (#53168) * Switch to dataclass from dictionary for climacell sensor definitions * fix post_init * fix dataclass and add test * Update homeassistant/components/climacell/sensor.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Update homeassistant/components/climacell/const.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * simplify logic * use tuple * simplify unit of measurement and use class attributes * Switch from UnitT to str Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/climacell/__init__.py | 8 +- homeassistant/components/climacell/const.py | 474 +++++++++--------- homeassistant/components/climacell/sensor.py | 105 ++-- tests/components/climacell/test_const.py | 12 + 4 files changed, 282 insertions(+), 317 deletions(-) create mode 100644 tests/components/climacell/test_const.py diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index db006b6fb68..9fd3e0b8340 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -35,7 +35,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - ATTR_FIELD, ATTRIBUTION, CC_ATTR_CLOUD_COVER, CC_ATTR_CONDITION, @@ -223,10 +222,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_PRECIPITATION_TYPE, - *( - sensor_type[ATTR_FIELD] - for sensor_type in CC_V3_SENSOR_TYPES - ), + *(sensor_type.field for sensor_type in CC_V3_SENSOR_TYPES), ] ) data[FORECASTS][HOURLY] = await self._api.forecast_hourly( @@ -283,7 +279,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_ATTR_WIND_GUST, CC_ATTR_CLOUD_COVER, CC_ATTR_PRECIPITATION_TYPE, - *(sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES), + *(sensor_type.field for sensor_type in CC_SENSOR_TYPES), ], [ CC_ATTR_TEMPERATURE_LOW, diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index f5d2ac02696..f19724b002e 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -1,4 +1,10 @@ """Constants for the ClimaCell integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum +from typing import Callable + from pyclimacell.const import ( DAILY, HOURLY, @@ -26,15 +32,10 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_NAME, CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - CONF_UNIT_OF_MEASUREMENT, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, DEVICE_CLASS_CO, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -74,13 +75,6 @@ MAX_FORECASTS = { NOWCAST: 30, } -# Sensor type keys -ATTR_FIELD = "field" -ATTR_METRIC_CONVERSION = "metric_conversion" -ATTR_VALUE_MAP = "value_map" -ATTR_IS_METRIC_CHECK = "is_metric_check" -ATTR_SCALE = "scale" - # Additional attributes ATTR_WIND_GUST = "wind_gust" ATTR_CLOUD_COVER = "cloud_cover" @@ -155,165 +149,182 @@ CC_ATTR_SOLAR_GHI = "solarGHI" CC_ATTR_CLOUD_BASE = "cloudBase" CC_ATTR_CLOUD_CEILING = "cloudCeiling" -CC_SENSOR_TYPES = [ - { - ATTR_FIELD: CC_ATTR_FEELS_LIKE, - ATTR_NAME: "Feels Like", - CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT, - CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS, - ATTR_METRIC_CONVERSION: lambda val: temp_convert( - val, TEMP_FAHRENHEIT, TEMP_CELSIUS - ), - ATTR_IS_METRIC_CHECK: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - { - ATTR_FIELD: CC_ATTR_DEW_POINT, - ATTR_NAME: "Dew Point", - CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT, - CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS, - ATTR_METRIC_CONVERSION: lambda val: temp_convert( - val, TEMP_FAHRENHEIT, TEMP_CELSIUS - ), - ATTR_IS_METRIC_CHECK: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - { - ATTR_FIELD: CC_ATTR_PRESSURE_SURFACE_LEVEL, - ATTR_NAME: "Pressure (Surface Level)", - CONF_UNIT_SYSTEM_IMPERIAL: PRESSURE_INHG, - CONF_UNIT_SYSTEM_METRIC: PRESSURE_HPA, - ATTR_METRIC_CONVERSION: lambda val: pressure_convert( + +@dataclass +class ClimaCellSensorMetadata: + """Metadata about an individual ClimaCell sensor.""" + + field: str + name: str + unit_imperial: str | None = None + unit_metric: str | None = None + metric_conversion: Callable | float = 1.0 + is_metric_check: bool | None = None + device_class: str | None = None + value_map: IntEnum | None = None + + def __post_init__(self) -> None: + """Post initialization.""" + units = (self.unit_imperial, self.unit_metric) + if any(u is not None for u in units) and any(u is None for u in units): + raise RuntimeError( + "`unit_imperial` and `unit_metric` both need to be None or both need " + "to be defined." + ) + + +CC_SENSOR_TYPES = ( + ClimaCellSensorMetadata( + CC_ATTR_FEELS_LIKE, + "Feels Like", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + ClimaCellSensorMetadata( + CC_ATTR_DEW_POINT, + "Dew Point", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + ClimaCellSensorMetadata( + CC_ATTR_PRESSURE_SURFACE_LEVEL, + "Pressure (Surface Level)", + unit_imperial=PRESSURE_INHG, + unit_metric=PRESSURE_HPA, + metric_conversion=lambda val: pressure_convert( val, PRESSURE_INHG, PRESSURE_HPA ), - ATTR_IS_METRIC_CHECK: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - }, - { - ATTR_FIELD: CC_ATTR_SOLAR_GHI, - ATTR_NAME: "Global Horizontal Irradiance", - CONF_UNIT_SYSTEM_IMPERIAL: IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, - CONF_UNIT_SYSTEM_METRIC: IRRADIATION_WATTS_PER_SQUARE_METER, - ATTR_METRIC_CONVERSION: 3.15459, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_CLOUD_BASE, - ATTR_NAME: "Cloud Base", - CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES, - CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS, - ATTR_METRIC_CONVERSION: lambda val: distance_convert( + is_metric_check=True, + device_class=DEVICE_CLASS_PRESSURE, + ), + ClimaCellSensorMetadata( + CC_ATTR_SOLAR_GHI, + "Global Horizontal Irradiance", + unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, + metric_conversion=3.15459, + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_CLOUD_BASE, + "Cloud Base", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( val, LENGTH_MILES, LENGTH_KILOMETERS ), - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_CLOUD_CEILING, - ATTR_NAME: "Cloud Ceiling", - CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES, - CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS, - ATTR_METRIC_CONVERSION: lambda val: distance_convert( + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_CLOUD_CEILING, + "Cloud Ceiling", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( val, LENGTH_MILES, LENGTH_KILOMETERS ), - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_CLOUD_COVER, - ATTR_NAME: "Cloud Cover", - CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - { - ATTR_FIELD: CC_ATTR_WIND_GUST, - ATTR_NAME: "Wind Gust", - CONF_UNIT_SYSTEM_IMPERIAL: SPEED_MILES_PER_HOUR, - CONF_UNIT_SYSTEM_METRIC: SPEED_METERS_PER_SECOND, - ATTR_METRIC_CONVERSION: lambda val: distance_convert( - val, LENGTH_MILES, LENGTH_METERS - ) + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_CLOUD_COVER, + "Cloud Cover", + unit_imperial=PERCENTAGE, + unit_metric=PERCENTAGE, + ), + ClimaCellSensorMetadata( + CC_ATTR_WIND_GUST, + "Wind Gust", + unit_imperial=SPEED_MILES_PER_HOUR, + unit_metric=SPEED_METERS_PER_SECOND, + metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) / 3600, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_PRECIPITATION_TYPE, - ATTR_NAME: "Precipitation Type", - ATTR_VALUE_MAP: PrecipitationType, - }, - { - ATTR_FIELD: CC_ATTR_OZONE, - ATTR_NAME: "Ozone", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, - ATTR_NAME: "Particulate Matter < 2.5 μm", - CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, - ATTR_NAME: "Particulate Matter < 10 μm", - CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_NITROGEN_DIOXIDE, - ATTR_NAME: "Nitrogen Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_ATTR_CARBON_MONOXIDE, - ATTR_NAME: "Carbon Monoxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - }, - { - ATTR_FIELD: CC_ATTR_SULFUR_DIOXIDE, - ATTR_NAME: "Sulfur Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - {ATTR_FIELD: CC_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, - { - ATTR_FIELD: CC_ATTR_EPA_PRIMARY_POLLUTANT, - ATTR_NAME: "US EPA Primary Pollutant", - ATTR_VALUE_MAP: PrimaryPollutantType, - }, - { - ATTR_FIELD: CC_ATTR_EPA_HEALTH_CONCERN, - ATTR_NAME: "US EPA Health Concern", - ATTR_VALUE_MAP: HealthConcernType, - }, - {ATTR_FIELD: CC_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, - { - ATTR_FIELD: CC_ATTR_CHINA_PRIMARY_POLLUTANT, - ATTR_NAME: "China MEP Primary Pollutant", - ATTR_VALUE_MAP: PrimaryPollutantType, - }, - { - ATTR_FIELD: CC_ATTR_CHINA_HEALTH_CONCERN, - ATTR_NAME: "China MEP Health Concern", - ATTR_VALUE_MAP: HealthConcernType, - }, - { - ATTR_FIELD: CC_ATTR_POLLEN_TREE, - ATTR_NAME: "Tree Pollen Index", - ATTR_VALUE_MAP: PollenIndex, - }, - { - ATTR_FIELD: CC_ATTR_POLLEN_WEED, - ATTR_NAME: "Weed Pollen Index", - ATTR_VALUE_MAP: PollenIndex, - }, - { - ATTR_FIELD: CC_ATTR_POLLEN_GRASS, - ATTR_NAME: "Grass Pollen Index", - ATTR_VALUE_MAP: PollenIndex, - }, - {ATTR_FIELD: CC_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, -] + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_PRECIPITATION_TYPE, + "Precipitation Type", + value_map=PrecipitationType, + ), + ClimaCellSensorMetadata( + CC_ATTR_OZONE, + "Ozone", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_ATTR_PARTICULATE_MATTER_25, + "Particulate Matter < 2.5 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_PARTICULATE_MATTER_10, + "Particulate Matter < 10 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_NITROGEN_DIOXIDE, + "Nitrogen Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_ATTR_CARBON_MONOXIDE, + "Carbon Monoxide", + unit_imperial=CONCENTRATION_PARTS_PER_MILLION, + unit_metric=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO, + ), + ClimaCellSensorMetadata( + CC_ATTR_SULFUR_DIOXIDE, + "Sulfur Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata(CC_ATTR_EPA_AQI, "US EPA Air Quality Index"), + ClimaCellSensorMetadata( + CC_ATTR_EPA_PRIMARY_POLLUTANT, + "US EPA Primary Pollutant", + value_map=PrimaryPollutantType, + ), + ClimaCellSensorMetadata( + CC_ATTR_EPA_HEALTH_CONCERN, + "US EPA Health Concern", + value_map=HealthConcernType, + ), + ClimaCellSensorMetadata(CC_ATTR_CHINA_AQI, "China MEP Air Quality Index"), + ClimaCellSensorMetadata( + CC_ATTR_CHINA_PRIMARY_POLLUTANT, + "China MEP Primary Pollutant", + value_map=PrimaryPollutantType, + ), + ClimaCellSensorMetadata( + CC_ATTR_CHINA_HEALTH_CONCERN, + "China MEP Health Concern", + value_map=HealthConcernType, + ), + ClimaCellSensorMetadata( + CC_ATTR_POLLEN_TREE, "Tree Pollen Index", value_map=PollenIndex + ), + ClimaCellSensorMetadata( + CC_ATTR_POLLEN_WEED, "Weed Pollen Index", value_map=PollenIndex + ), + ClimaCellSensorMetadata( + CC_ATTR_POLLEN_GRASS, "Grass Pollen Index", value_map=PollenIndex + ), + ClimaCellSensorMetadata(CC_ATTR_FIRE_INDEX, "Fire Index"), +) # V3 constants CONDITIONS_V3 = { @@ -377,73 +388,68 @@ CC_V3_ATTR_POLLEN_WEED = "pollen_weed" CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" CC_V3_ATTR_FIRE_INDEX = "fire_index" -CC_V3_SENSOR_TYPES = [ - { - ATTR_FIELD: CC_V3_ATTR_OZONE, - ATTR_NAME: "Ozone", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_25, - ATTR_NAME: "Particulate Matter < 2.5 μm", - CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: False, - }, - { - ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_10, - ATTR_NAME: "Particulate Matter < 10 μm", - CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: False, - }, - { - ATTR_FIELD: CC_V3_ATTR_NITROGEN_DIOXIDE, - ATTR_NAME: "Nitrogen Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_V3_ATTR_CARBON_MONOXIDE, - ATTR_NAME: "Carbon Monoxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - }, - { - ATTR_FIELD: CC_V3_ATTR_SULFUR_DIOXIDE, - ATTR_NAME: "Sulfur Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - {ATTR_FIELD: CC_V3_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, - { - ATTR_FIELD: CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, - ATTR_NAME: "US EPA Primary Pollutant", - }, - {ATTR_FIELD: CC_V3_ATTR_EPA_HEALTH_CONCERN, ATTR_NAME: "US EPA Health Concern"}, - {ATTR_FIELD: CC_V3_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, - { - ATTR_FIELD: CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, - ATTR_NAME: "China MEP Primary Pollutant", - }, - { - ATTR_FIELD: CC_V3_ATTR_CHINA_HEALTH_CONCERN, - ATTR_NAME: "China MEP Health Concern", - }, - { - ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, - ATTR_NAME: "Tree Pollen Index", - ATTR_VALUE_MAP: V3PollenIndex, - }, - { - ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, - ATTR_NAME: "Weed Pollen Index", - ATTR_VALUE_MAP: V3PollenIndex, - }, - { - ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, - ATTR_NAME: "Grass Pollen Index", - ATTR_VALUE_MAP: V3PollenIndex, - }, - {ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, -] +CC_V3_SENSOR_TYPES = ( + ClimaCellSensorMetadata( + CC_V3_ATTR_OZONE, + "Ozone", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_PARTICULATE_MATTER_25, + "Particulate Matter < 2.5 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=False, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_PARTICULATE_MATTER_10, + "Particulate Matter < 10 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=False, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_NITROGEN_DIOXIDE, + "Nitrogen Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_CARBON_MONOXIDE, + "Carbon Monoxide", + unit_imperial=CONCENTRATION_PARTS_PER_MILLION, + unit_metric=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_SULFUR_DIOXIDE, + "Sulfur Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata(CC_V3_ATTR_EPA_AQI, "US EPA Air Quality Index"), + ClimaCellSensorMetadata( + CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, "US EPA Primary Pollutant" + ), + ClimaCellSensorMetadata(CC_V3_ATTR_EPA_HEALTH_CONCERN, "US EPA Health Concern"), + ClimaCellSensorMetadata(CC_V3_ATTR_CHINA_AQI, "China MEP Air Quality Index"), + ClimaCellSensorMetadata( + CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, "China MEP Primary Pollutant" + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_CHINA_HEALTH_CONCERN, "China MEP Health Concern" + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_POLLEN_TREE, "Tree Pollen Index", value_map=V3PollenIndex + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_POLLEN_WEED, "Weed Pollen Index", value_map=V3PollenIndex + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_POLLEN_GRASS, "Grass Pollen Index", value_map=V3PollenIndex + ), + ClimaCellSensorMetadata(CC_V3_ATTR_FIRE_INDEX, "Fire Index"), +) diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index e5d9a2cdb75..77fa6486013 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -2,39 +2,19 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Mapping import logging -from typing import Any from pyclimacell.const import CURRENT from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_NAME, - CONF_API_VERSION, - CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_VERSION, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import ( - ATTR_FIELD, - ATTR_IS_METRIC_CHECK, - ATTR_METRIC_CONVERSION, - ATTR_SCALE, - ATTR_VALUE_MAP, - CC_SENSOR_TYPES, - CC_V3_SENSOR_TYPES, - DOMAIN, -) +from .const import CC_SENSOR_TYPES, CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorMetadata _LOGGER = logging.getLogger(__name__) @@ -55,7 +35,7 @@ async def async_setup_entry( api_class = ClimaCellSensorEntity sensor_types = CC_SENSOR_TYPES entities = [ - api_class(config_entry, coordinator, api_version, sensor_type) + api_class(hass, config_entry, coordinator, api_version, sensor_type) for sensor_type in sensor_types ] async_add_entities(entities) @@ -66,53 +46,29 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): def __init__( self, + hass: HomeAssistant, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator, api_version: int, - sensor_type: dict[str, str | float], + sensor_type: ClimaCellSensorMetadata, ) -> None: """Initialize ClimaCell Sensor Entity.""" super().__init__(config_entry, coordinator, api_version) self.sensor_type = sensor_type - self._attr_device_class = self.sensor_type.get(ATTR_DEVICE_CLASS) - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type[ATTR_NAME]}" - - @property - def unique_id(self) -> str: - """Return the unique id of the entity.""" - return f"{self._config_entry.unique_id}_{slugify(self.sensor_type[ATTR_NAME])}" - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - return {ATTR_ATTRIBUTION: self.attribution} - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - if CONF_UNIT_OF_MEASUREMENT in self.sensor_type: - return self.sensor_type[CONF_UNIT_OF_MEASUREMENT] - - if ( - CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type - and CONF_UNIT_SYSTEM_METRIC in self.sensor_type - ): - return ( - self.sensor_type[CONF_UNIT_SYSTEM_METRIC] - if self.hass.config.units.is_metric - else self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] - ) - - return None + self._attr_device_class = self.sensor_type.device_class + self._attr_entity_registry_enabled_default = False + self._attr_name = ( + f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type.name}" + ) + self._attr_unique_id = ( + f"{self._config_entry.unique_id}_{slugify(self.sensor_type.name)}" + ) + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} + self._attr_unit_of_measurement = ( + self.sensor_type.unit_metric + if hass.config.units.is_metric + else self.sensor_type.unit_imperial + ) @property @abstractmethod @@ -123,27 +79,22 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): def state(self) -> str | int | float | None: """Return the state.""" state = self._state - if state and ATTR_SCALE in self.sensor_type: - state *= self.sensor_type[ATTR_SCALE] - if ( state is not None - and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type - and CONF_UNIT_SYSTEM_METRIC in self.sensor_type - and ATTR_METRIC_CONVERSION in self.sensor_type - and ATTR_IS_METRIC_CHECK in self.sensor_type - and self.hass.config.units.is_metric - == self.sensor_type[ATTR_IS_METRIC_CHECK] + and self.sensor_type.unit_imperial is not None + and self.sensor_type.metric_conversion != 1.0 + and self.sensor_type.is_metric_check is not None + and self.hass.config.units.is_metric == self.sensor_type.is_metric_check ): - conversion = self.sensor_type[ATTR_METRIC_CONVERSION] + conversion = self.sensor_type.metric_conversion # When conversion is a callable, we assume it's a single input function if callable(conversion): return round(conversion(state), 4) return round(state * conversion, 4) - if ATTR_VALUE_MAP in self.sensor_type and state is not None: - return self.sensor_type[ATTR_VALUE_MAP](state).name.lower() + if self.sensor_type.value_map is not None and state is not None: + return self.sensor_type.value_map(state).name.lower() return state @@ -154,7 +105,7 @@ class ClimaCellSensorEntity(BaseClimaCellSensorEntity): @property def _state(self) -> str | int | float | None: """Return the raw state.""" - return self._get_current_property(self.sensor_type[ATTR_FIELD]) + return self._get_current_property(self.sensor_type.field) class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): @@ -164,5 +115,5 @@ class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): def _state(self) -> str | int | float | None: """Return the raw state.""" return self._get_cc_value( - self.coordinator.data[CURRENT], self.sensor_type[ATTR_FIELD] + self.coordinator.data[CURRENT], self.sensor_type.field ) diff --git a/tests/components/climacell/test_const.py b/tests/components/climacell/test_const.py new file mode 100644 index 00000000000..ba8eb6d8f39 --- /dev/null +++ b/tests/components/climacell/test_const.py @@ -0,0 +1,12 @@ +"""Tests for ClimaCell const.""" +import pytest + +from homeassistant.components.climacell.const import ClimaCellSensorMetadata +from homeassistant.const import TEMP_FAHRENHEIT + + +async def test_post_init(): + """Test post initiailization check for ClimaCellSensorMetadata.""" + + with pytest.raises(RuntimeError): + ClimaCellSensorMetadata("a", "b", unit_imperial=TEMP_FAHRENHEIT) From f0b28c90bfaad01681e5236a30a180c9e27ad638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 06:30:00 +0200 Subject: [PATCH 390/818] Co2signal configflow (#53193) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 - .../components/co2signal/__init__.py | 21 +- .../components/co2signal/config_flow.py | 249 +++++++++++++++ homeassistant/components/co2signal/const.py | 11 + .../components/co2signal/manifest.json | 9 +- homeassistant/components/co2signal/sensor.py | 90 +++--- .../components/co2signal/strings.json | 34 ++ .../components/co2signal/translations/en.json | 34 ++ homeassistant/components/co2signal/util.py | 18 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/co2signal/__init__.py | 11 + .../components/co2signal/test_config_flow.py | 299 ++++++++++++++++++ 13 files changed, 740 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/co2signal/config_flow.py create mode 100644 homeassistant/components/co2signal/const.py create mode 100644 homeassistant/components/co2signal/strings.json create mode 100644 homeassistant/components/co2signal/translations/en.json create mode 100644 homeassistant/components/co2signal/util.py create mode 100644 tests/components/co2signal/__init__.py create mode 100644 tests/components/co2signal/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2f60e74480f..3b6c140c0f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -152,7 +152,6 @@ omit = homeassistant/components/clicksend/notify.py homeassistant/components/clicksend_tts/notify.py homeassistant/components/cmus/media_player.py - homeassistant/components/co2signal/* homeassistant/components/coinbase/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py homeassistant/components/comfoconnect/fan.py diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index a9c6422b4c6..50a453ac5f3 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -1 +1,20 @@ -"""The co2signal component.""" +"""The CO2 Signal integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN # noqa: F401 + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up CO2 Signal from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py new file mode 100644 index 00000000000..044dd95cc3b --- /dev/null +++ b/homeassistant/components/co2signal/config_flow.py @@ -0,0 +1,249 @@ +"""Config flow for Co2signal integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import CO2Signal +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv + +from .const import CONF_COUNTRY_CODE, DOMAIN +from .util import get_extra_name + +_LOGGER = logging.getLogger(__name__) + +TYPE_USE_HOME = "Use home location" +TYPE_SPECIFY_COORDINATES = "Specify coordinates" +TYPE_SPECIFY_COUNTRY = "Specify country code" + + +def _get_entry_type(config: dict) -> str: + """Get entry type from the configuration.""" + if CONF_LATITUDE in config: + return TYPE_SPECIFY_COORDINATES + + if CONF_COUNTRY_CODE in config: + return TYPE_SPECIFY_COUNTRY + + return TYPE_USE_HOME + + +def _validate_info(hass, config: dict) -> dict: + """Validate the passed in info.""" + if CONF_COUNTRY_CODE in config: + latitude = None + longitude = None + else: + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + try: + data = CO2Signal.get_latest( + config[CONF_API_KEY], + config.get(CONF_COUNTRY_CODE), + latitude, + longitude, + wait=False, + ) + + except ValueError as err: + err_str = str(err) + + if "Invalid authentication credentials" in err_str: + raise InvalidAuth from err + if "API rate limit exceeded." in err_str: + raise APIRatelimitExceeded from err + + _LOGGER.exception("Unexpected exception") + raise UnknownError from err + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + raise UnknownError from err + + else: + if data.get("status") != "ok": + _LOGGER.exception("Unexpected response: %s", data) + raise UnknownError + + return data + + +class CO2Error(HomeAssistantError): + """Base error.""" + + +class InvalidAuth(CO2Error): + """Raised when invalid authentication credentials are provided.""" + + +class APIRatelimitExceeded(CO2Error): + """Raised when the API rate limit is exceeded.""" + + +class UnknownError(CO2Error): + """Raised when an unknown error occurs.""" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Co2signal.""" + + VERSION = 1 + _data: dict | None + + async def async_step_import(self, import_info): + """Set the config entry up from yaml.""" + data = {CONF_API_KEY: import_info[CONF_TOKEN]} + + if CONF_COUNTRY_CODE in import_info: + data[CONF_COUNTRY_CODE] = import_info[CONF_COUNTRY_CODE] + new_entry_type = TYPE_SPECIFY_COUNTRY + elif ( + CONF_LATITUDE in import_info + and import_info[CONF_LATITUDE] != self.hass.config.latitude + and import_info[CONF_LONGITUDE] != self.hass.config.longitude + ): + data[CONF_LATITUDE] = import_info[CONF_LATITUDE] + data[CONF_LONGITUDE] = import_info[CONF_LONGITUDE] + new_entry_type = TYPE_SPECIFY_COORDINATES + else: + new_entry_type = TYPE_USE_HOME + + for entry in self._async_current_entries(include_ignore=True): + if entry.source == config_entries.SOURCE_IGNORE: + continue + + if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type: + continue + + if cur_entry_type == TYPE_USE_HOME and new_entry_type == TYPE_USE_HOME: + return self.async_abort(reason="already_configured") + + if ( + cur_entry_type == TYPE_SPECIFY_COUNTRY + and data[CONF_COUNTRY_CODE] == entry.data[CONF_COUNTRY_CODE] + ): + return self.async_abort(reason="already_configured") + + if ( + cur_entry_type == TYPE_SPECIFY_COORDINATES + and data[CONF_LATITUDE] == entry.data[CONF_LATITUDE] + and data[CONF_LONGITUDE] == entry.data[CONF_LONGITUDE] + ): + return self.async_abort(reason="already_configured") + + try: + await self.hass.async_add_executor_job(_validate_info, self.hass, data) + except CO2Error: + return self.async_abort(reason="unknown") + + return self.async_create_entry( + title=get_extra_name(self.hass, data) or "CO2 Signal", data=data + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + data_schema = vol.Schema( + { + vol.Required("location", default=TYPE_USE_HOME): vol.In( + ( + TYPE_USE_HOME, + TYPE_SPECIFY_COORDINATES, + TYPE_SPECIFY_COUNTRY, + ) + ), + vol.Required(CONF_API_KEY): cv.string, + } + ) + + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=data_schema, + ) + + data = {CONF_API_KEY: user_input[CONF_API_KEY]} + + if user_input["location"] == TYPE_SPECIFY_COORDINATES: + self._data = data + return await self.async_step_coordinates() + + if user_input["location"] == TYPE_SPECIFY_COUNTRY: + self._data = data + return await self.async_step_country() + + return await self._validate_and_create("user", data_schema, data) + + async def async_step_coordinates( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Validate coordinates.""" + data_schema = vol.Schema( + { + vol.Required( + CONF_LATITUDE, + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, + ): cv.longitude, + } + ) + if user_input is None: + return self.async_show_form(step_id="coordinates", data_schema=data_schema) + + assert self._data is not None + + return await self._validate_and_create( + "coordinates", data_schema, {**self._data, **user_input} + ) + + async def async_step_country( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Validate country.""" + data_schema = vol.Schema( + { + vol.Required(CONF_COUNTRY_CODE): cv.string, + } + ) + if user_input is None: + return self.async_show_form(step_id="country", data_schema=data_schema) + + assert self._data is not None + + return await self._validate_and_create( + "country", data_schema, {**self._data, **user_input} + ) + + async def _validate_and_create( + self, step_id: str, data_schema: vol.Schema, data: dict + ) -> FlowResult: + """Validate data and show form if it is invalid.""" + errors: dict[str, str] = {} + + try: + await self.hass.async_add_executor_job(_validate_info, self.hass, data) + except InvalidAuth: + errors["base"] = "invalid_auth" + except APIRatelimitExceeded: + errors["base"] = "api_ratelimit" + except UnknownError: + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=get_extra_name(self.hass, data) or "CO2 Signal", + data=data, + ) + + return self.async_show_form( + step_id=step_id, + data_schema=data_schema, + errors=errors, + ) diff --git a/homeassistant/components/co2signal/const.py b/homeassistant/components/co2signal/const.py new file mode 100644 index 00000000000..1db0ccc20fd --- /dev/null +++ b/homeassistant/components/co2signal/const.py @@ -0,0 +1,11 @@ +"""Constants for the Co2signal integration.""" + + +DOMAIN = "co2signal" +CONF_COUNTRY_CODE = "country_code" +ATTRIBUTION = "Data provided by CO2signal" +MSG_LOCATION = ( + "Please use either coordinates or the country code. " + "For the coordinates, " + "you need to use both latitude and longitude." +) diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 50ed7f62038..1921ae4f575 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -2,7 +2,10 @@ "domain": "co2signal", "name": "CO2 Signal", "documentation": "https://www.home-assistant.io/integrations/co2signal", - "requirements": ["co2signal==0.4.2"], + "requirements": [ + "co2signal==0.4.2" + ], "codeowners": [], - "iot_class": "cloud_polling" -} + "iot_class": "cloud_polling", + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index cb2ff40c062..88a80df0b54 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,9 +5,14 @@ import logging import CO2Signal import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_NAME, + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN, @@ -15,18 +20,12 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -CONF_COUNTRY_CODE = "country_code" +from .const import ATTRIBUTION, CONF_COUNTRY_CODE, DOMAIN, MSG_LOCATION +from .util import get_extra_name _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=3) -ATTRIBUTION = "Data provided by CO2signal" - -MSG_LOCATION = ( - "Please use either coordinates or the country code. " - "For the coordinates, " - "you need to use both latitude and longitude." -) CO2_INTENSITY_UNIT = f"CO2eq/{ENERGY_KILO_WATT_HOUR}" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -38,16 +37,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the CO2signal sensor.""" - token = config[CONF_TOKEN] - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - country_code = config.get(CONF_COUNTRY_CODE) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config, + ) - _LOGGER.debug("Setting up the sensor using the %s", country_code) - add_entities([CO2Sensor(token, country_code, lat, lon)], True) +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the CO2signal sensor.""" + name = "CO2 intensity" + if extra_name := get_extra_name(hass, entry.data): + name += f" - {extra_name}" + + async_add_entities( + [ + CO2Sensor( + name, + entry.data, + entry_id=entry.entry_id, + ) + ], + True, + ) class CO2Sensor(SensorEntity): @@ -56,33 +70,37 @@ class CO2Sensor(SensorEntity): _attr_icon = "mdi:molecule-co2" _attr_unit_of_measurement = CO2_INTENSITY_UNIT - def __init__(self, token, country_code, lat, lon): + def __init__(self, name, config, entry_id): """Initialize the sensor.""" - self._token = token - self._country_code = country_code - self._latitude = lat - self._longitude = lon - - if country_code is not None: - device_name = country_code - else: - device_name = f"{round(self._latitude, 2)}/{round(self._longitude, 2)}" - - self._attr_name = f"CO2 intensity - {device_name}" + self._config = config + self._attr_name = name self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, entry_id)}, + ATTR_NAME: "CO2 signal", + ATTR_MANUFACTURER: "Tmrow.com", + "entry_type": "service", + } + self._attr_unique_id = f"{entry_id}_co2intensity" def update(self): """Get the latest data and updates the states.""" - _LOGGER.debug("Update data for %s", self.name) - if self._country_code is not None: - data = CO2Signal.get_latest_carbon_intensity( - self._token, country_code=self._country_code - ) + if CONF_COUNTRY_CODE in self._config: + kwargs = {"country_code": self._config[CONF_COUNTRY_CODE]} + elif CONF_LATITUDE in self._config: + kwargs = { + "latitude": self._config[CONF_LATITUDE], + "longitude": self._config[CONF_LONGITUDE], + } else: - data = CO2Signal.get_latest_carbon_intensity( - self._token, latitude=self._latitude, longitude=self._longitude - ) + kwargs = { + "latitude": self.hass.config.latitude, + "longitude": self.hass.config.longitude, + } - self._attr_state = round(data, 2) + self._attr_state = round( + CO2Signal.get_latest_carbon_intensity(self._config[CONF_API_KEY], **kwargs), + 2, + ) diff --git a/homeassistant/components/co2signal/strings.json b/homeassistant/components/co2signal/strings.json new file mode 100644 index 00000000000..2fe5b79c907 --- /dev/null +++ b/homeassistant/components/co2signal/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Get data for", + "api_key": "[%key:common::config_flow::data::access_token%]" + }, + "description": "Visit https://co2signal.com/ to request a token." + }, + "coordinates": { + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + }, + "country": { + "data": { + "country_code": "Country code" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "api_ratelimit": "API Ratelimit exceeded" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "api_ratelimit": "API Ratelimit exceeded" + } + } +} diff --git a/homeassistant/components/co2signal/translations/en.json b/homeassistant/components/co2signal/translations/en.json new file mode 100644 index 00000000000..3d8cc7c9d9f --- /dev/null +++ b/homeassistant/components/co2signal/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "api_ratelimit": "API Ratelimit exceeded", + "unknown": "Unexpected error" + }, + "error": { + "api_ratelimit": "API Ratelimit exceeded", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "country": { + "data": { + "country_code": "Country code" + } + }, + "user": { + "data": { + "api_key": "Access Token", + "location": "Get data for" + }, + "description": "Visit https://co2signal.com/ to request a token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/util.py b/homeassistant/components/co2signal/util.py new file mode 100644 index 00000000000..9cda6f558bf --- /dev/null +++ b/homeassistant/components/co2signal/util.py @@ -0,0 +1,18 @@ +"""Utils for CO2 signal.""" +from __future__ import annotations + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from .const import CONF_COUNTRY_CODE + + +def get_extra_name(hass: HomeAssistant, config: dict) -> str | None: + """Return the extra name describing the location if not home.""" + if CONF_COUNTRY_CODE in config: + return config[CONF_COUNTRY_CODE] + + if CONF_LATITUDE in config: + return f"{round(config[CONF_LATITUDE], 2)}, {round(config[CONF_LONGITUDE], 2)}" + + return None diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 40b5f1360ed..45a339ebed1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ FLOWS = [ "cert_expiry", "climacell", "cloudflare", + "co2signal", "coinbase", "control4", "coolmaster", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7674a78ce9e..f02e611debf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -249,6 +249,9 @@ buienradar==1.0.4 # homeassistant.components.caldav caldav==0.7.1 +# homeassistant.components.co2signal +co2signal==0.4.2 + # homeassistant.components.coinbase coinbase==2.1.0 diff --git a/tests/components/co2signal/__init__.py b/tests/components/co2signal/__init__.py new file mode 100644 index 00000000000..1f3d6a83c05 --- /dev/null +++ b/tests/components/co2signal/__init__.py @@ -0,0 +1,11 @@ +"""Tests for the CO2 Signal integration.""" + +VALID_PAYLOAD = { + "status": "ok", + "countryCode": "FR", + "data": { + "carbonIntensity": 45.98623190095805, + "fossilFuelPercentage": 5.461182741937103, + }, + "units": {"carbonIntensity": "gCO2eq/kWh"}, +} diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py new file mode 100644 index 00000000000..25d38526133 --- /dev/null +++ b/tests/components/co2signal/test_config_flow.py @@ -0,0 +1,299 @@ +"""Test the CO2 Signal config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.co2signal import DOMAIN, config_flow +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.setup import async_setup_component + +from . import VALID_PAYLOAD + +from tests.common import MockConfigEntry + + +async def test_form_home(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ), patch( + "homeassistant.components.co2signal.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "location": config_flow.TYPE_USE_HOME, + "api_key": "api_key", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "CO2 Signal" + assert result2["data"] == { + "api_key": "api_key", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_coordinates(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "location": config_flow.TYPE_SPECIFY_COORDINATES, + "api_key": "api_key", + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ), patch( + "homeassistant.components.co2signal.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "latitude": 12.3, + "longitude": 45.6, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "12.3, 45.6" + assert result3["data"] == { + "latitude": 12.3, + "longitude": 45.6, + "api_key": "api_key", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_country(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "location": config_flow.TYPE_SPECIFY_COUNTRY, + "api_key": "api_key", + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ), patch( + "homeassistant.components.co2signal.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "country_code": "fr", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "fr" + assert result3["data"] == { + "country_code": "fr", + "api_key": "api_key", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "err_str,err_code", + [ + ("Invalid authentication credentials", "invalid_auth"), + ("API rate limit exceeded.", "api_ratelimit"), + ("Something else", "unknown"), + ], +) +async def test_form_error_handling(hass: HomeAssistant, err_str, err_code) -> None: + """Test we handle expected errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + side_effect=ValueError(err_str), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "location": config_flow.TYPE_USE_HOME, + "api_key": "api_key", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": err_code} + + +async def test_form_error_unexpected_error(hass: HomeAssistant) -> None: + """Test we handle unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + side_effect=Exception("Boom"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "location": config_flow.TYPE_USE_HOME, + "api_key": "api_key", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: + """Test we handle unexpected data.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value={"status": "error"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "location": config_flow.TYPE_USE_HOME, + "api_key": "api_key", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test we import correctly.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ): + assert await async_setup_component( + hass, "sensor", {"sensor": {"platform": "co2signal", "token": "1234"}} + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries("co2signal")) == 1 + state = hass.states.get("sensor.co2_intensity") + assert state is not None + assert state.state == "45.99" + assert state.name == "CO2 intensity" + + +async def test_import_abort_existing_home(hass: HomeAssistant) -> None: + """Test we abort if home entry found.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry(domain="co2signal", data={"api_key": "abcd"}).add_to_hass(hass) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ): + assert await async_setup_component( + hass, "sensor", {"sensor": {"platform": "co2signal", "token": "1234"}} + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries("co2signal")) == 1 + + +async def test_import_abort_existing_country(hass: HomeAssistant) -> None: + """Test we abort if existing country found.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry( + domain="co2signal", data={"api_key": "abcd", "country_code": "nl"} + ).add_to_hass(hass) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ): + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "co2signal", + "token": "1234", + "country_code": "nl", + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries("co2signal")) == 1 + + +async def test_import_abort_existing_coordinates(hass: HomeAssistant) -> None: + """Test we abort if existing coordinates found.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry( + domain="co2signal", data={"api_key": "abcd", "latitude": 1, "longitude": 2} + ).add_to_hass(hass) + + with patch( + "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ): + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "co2signal", + "token": "1234", + "latitude": 1, + "longitude": 2, + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries("co2signal")) == 1 From e8d7952880cb0c8fa20ea500b9dfb44c4733c663 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 20 Jul 2021 06:39:19 +0200 Subject: [PATCH 391/818] Refactor KNX tests (#53183) * refactor tests for KNX - implement KNXTestKit class for convenient setup and assertion of KNX telegrams - add fixture returning an instance of KNXTestKit with automatic cleanup test * add tests for expose default attribute - fix expose edge case not covered by #53046 * use asyncio.Queue instead of AsyncMock.call_args_list for better readability * get xknx from Mock instead of hass.data * fix type annotations * add injection methods for incoming telegrams * rest read-response in expose --- homeassistant/components/knx/expose.py | 3 +- tests/components/knx/__init__.py | 26 ---- tests/components/knx/conftest.py | 188 +++++++++++++++++++++++-- tests/components/knx/test_expose.py | 117 +++++++++------ 4 files changed, 254 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 5b92f9f1f6a..408ab25e7cc 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -141,7 +141,8 @@ class KNXExposeSensor: if new_value is None: return old_state = event.data.get("old_state") - old_value = self._get_expose_value(old_state) + # don't use default value for comparison on first state change (old_state is None) + old_value = self._get_expose_value(old_state) if old_state is not None else None # don't send same value sequentially if new_value != old_value: await self._async_set_knx_value(new_value) diff --git a/tests/components/knx/__init__.py b/tests/components/knx/__init__.py index 1c9bfaf15b8..eaa84714dc5 100644 --- a/tests/components/knx/__init__.py +++ b/tests/components/knx/__init__.py @@ -1,27 +1 @@ """Tests for the KNX integration.""" - -from unittest.mock import DEFAULT, patch - -from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN -from homeassistant.setup import async_setup_component - - -async def setup_knx_integration(hass, knx_ip_interface, config=None): - """Create the KNX gateway.""" - if config is None: - config = {} - - # To get the XKNX object from the constructor call - def side_effect(*args, **kwargs): - knx_ip_interface.xknx = args[0] - # switch off rate delimiter - knx_ip_interface.xknx.rate_limit = 0 - return DEFAULT - - with patch( - "xknx.xknx.KNXIPInterface", - return_value=knx_ip_interface, - side_effect=side_effect, - ): - await async_setup_component(hass, KNX_DOMAIN, {KNX_DOMAIN: config}) - await hass.async_block_till_done() diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index b7c27774f78..548e620813a 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -1,15 +1,183 @@ -"""conftest for knx.""" +"""Conftest for the KNX integration.""" +from __future__ import annotations -from unittest.mock import AsyncMock, Mock +import asyncio +from unittest.mock import DEFAULT, AsyncMock, Mock, patch import pytest +from xknx import XKNX +from xknx.dpt import DPTArray, DPTBinary +from xknx.telegram import Telegram, TelegramDirection +from xknx.telegram.address import GroupAddress, IndividualAddress +from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite + +from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component -@pytest.fixture(autouse=True) -def knx_ip_interface_mock(): - """Create a knx ip interface mock.""" - mock = Mock() - mock.start = AsyncMock() - mock.stop = AsyncMock() - mock.send_telegram = AsyncMock() - return mock +class KNXTestKit: + """Test helper for the KNX integration.""" + + def __init__(self, hass: HomeAssistant): + """Init KNX test helper class.""" + self.hass: HomeAssistant = hass + self.xknx: XKNX + # outgoing telegrams will be put in the Queue instead of sent to the interface + # telegrams to an InternalGroupAddress won't be queued here + self._outgoing_telegrams: asyncio.Queue = asyncio.Queue() + + async def setup_integration(self, config): + """Create the KNX integration.""" + + def knx_ip_interface_mock(): + """Create a xknx knx ip interface mock.""" + mock = Mock() + mock.start = AsyncMock() + mock.stop = AsyncMock() + mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) + return mock + + def fish_xknx(*args, **kwargs): + """Get the XKNX object from the constructor call.""" + self.xknx = args[0] + return DEFAULT + + with patch( + "xknx.xknx.KNXIPInterface", + return_value=knx_ip_interface_mock(), + side_effect=fish_xknx, + ): + await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) + await self.hass.async_block_till_done() + # disable rate limiter for tests + self.xknx.rate_limit = 0 + + ######################## + # Telegram counter tests + ######################## + + def _list_remaining_telegrams(self) -> str: + """Return a string containing remaining outgoing telegrams in test Queue. One per line.""" + remaining_telegrams = [] + while not self._outgoing_telegrams.empty(): + remaining_telegrams.append(self._outgoing_telegrams.get_nowait()) + return "\n".join(map(str, remaining_telegrams)) + + async def assert_no_telegram(self) -> None: + """Assert if every telegram in test Queue was checked.""" + await self.hass.async_block_till_done() + assert self._outgoing_telegrams.empty(), ( + f"Found remaining unasserted Telegrams: {self._outgoing_telegrams.qsize()}\n" + f"{self._list_remaining_telegrams()}" + ) + + async def assert_telegram_count(self, count: int) -> None: + """Assert outgoing telegram count in test Queue.""" + await self.hass.async_block_till_done() + actual_count = self._outgoing_telegrams.qsize() + assert actual_count == count, ( + f"Outgoing telegrams: {actual_count} - Expected: {count}\n" + f"{self._list_remaining_telegrams()}" + ) + + #################### + # APCI Service tests + #################### + + async def _assert_telegram( + self, + group_address: str, + payload: int | tuple[int, ...] | None, + apci_type: type[APCI], + ) -> None: + """Assert outgoing telegram. One by one in timely order.""" + await self.hass.async_block_till_done() + try: + telegram = self._outgoing_telegrams.get_nowait() + except asyncio.QueueEmpty: + raise AssertionError( + f"No Telegram found. Expected: {apci_type.__name__} -" + f" {group_address} - {payload}" + ) + + assert ( + str(telegram.destination_address) == group_address + ), f"Group address mismatch in {telegram} - Expected: {group_address}" + + assert isinstance( + telegram.payload, apci_type + ), f"APCI type mismatch in {telegram} - Expected: {apci_type.__name__}" + + if payload is not None: + assert ( + telegram.payload.value.value == payload # type: ignore + ), f"Payload mismatch in {telegram} - Expected: {payload}" + + async def assert_read(self, group_address: str) -> None: + """Assert outgoing GroupValueRead telegram. One by one in timely order.""" + await self._assert_telegram(group_address, None, GroupValueRead) + + async def assert_response( + self, group_address: str, payload: int | tuple[int, ...] + ) -> None: + """Assert outgoing GroupValueResponse telegram. One by one in timely order.""" + await self._assert_telegram(group_address, payload, GroupValueResponse) + + async def assert_write( + self, group_address: str, payload: int | tuple[int, ...] + ) -> None: + """Assert outgoing GroupValueWrite telegram. One by one in timely order.""" + await self._assert_telegram(group_address, payload, GroupValueWrite) + + #################### + # Incoming telegrams + #################### + + async def _receive_telegram(self, group_address: str, payload: APCI) -> None: + """Inject incoming KNX telegram.""" + self.xknx.telegrams.put_nowait( + Telegram( + destination_address=GroupAddress(group_address), + direction=TelegramDirection.INCOMING, + payload=payload, + source_address=IndividualAddress("1.2.3"), + ) + ) + await self.hass.async_block_till_done() + + @staticmethod + def _payload_value(payload: int | tuple[int, ...]) -> DPTArray | DPTBinary: + """Prepare payload value for GroupValueWrite or GroupValueResponse.""" + if isinstance(payload, int): + return DPTBinary(payload) + return DPTArray(payload) + + async def receive_read( + self, + group_address: str, + ) -> None: + """Inject incoming GroupValueRead telegram.""" + await self._receive_telegram(group_address, GroupValueRead()) + + async def receive_response( + self, group_address: str, payload: int | tuple[int, ...] + ) -> None: + """Inject incoming GroupValueResponse telegram.""" + payload_value = self._payload_value(payload) + await self._receive_telegram(group_address, GroupValueResponse(payload_value)) + + async def receive_write( + self, group_address: str, payload: int | tuple[int, ...] + ) -> None: + """Inject incoming GroupValueWrite telegram.""" + payload_value = self._payload_value(payload) + await self._receive_telegram(group_address, GroupValueWrite(payload_value)) + + +@pytest.fixture +async def knx(request, hass): + """Create a KNX TestKit instance.""" + knx_test_kit = KNXTestKit(hass) + yield knx_test_kit + await knx_test_kit.assert_no_telegram() diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 908ef0a56f8..6922b6ed926 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -1,18 +1,13 @@ -"""Test knx expose.""" - - +"""Test KNX expose.""" from homeassistant.components.knx import CONF_KNX_EXPOSE, KNX_ADDRESS +from homeassistant.components.knx.schema import ExposeSchema from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_TYPE -from . import setup_knx_integration - -async def test_binary_expose(hass, knx_ip_interface_mock): - """Test that a binary expose sends only telegrams on state change.""" +async def test_binary_expose(hass, knx): + """Test a binary expose to only send telegrams on state change.""" entity_id = "fake.entity" - await setup_knx_integration( - hass, - knx_ip_interface_mock, + await knx.setup_integration( { CONF_KNX_EXPOSE: { CONF_TYPE: "binary", @@ -24,37 +19,23 @@ async def test_binary_expose(hass, knx_ip_interface_mock): assert not hass.states.async_all() # Change state to on - knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {}) - await hass.async_block_till_done() - assert ( - knx_ip_interface_mock.send_telegram.call_count == 1 - ), "Expected telegram for state change" + await knx.assert_write("1/1/8", True) # Change attribute; keep state - knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {"brightness": 180}) - await hass.async_block_till_done() - assert ( - knx_ip_interface_mock.send_telegram.call_count == 0 - ), "Expected no telegram; state not changed" + await knx.assert_no_telegram() # Change attribute and state - knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "off", {"brightness": 0}) - await hass.async_block_till_done() - assert ( - knx_ip_interface_mock.send_telegram.call_count == 1 - ), "Expected telegram for state change" + await knx.assert_write("1/1/8", False) -async def test_expose_attribute(hass, knx_ip_interface_mock): - """Test that an expose sends only telegrams on attribute change.""" +async def test_expose_attribute(hass, knx): + """Test an expose to only send telegrams on attribute change.""" entity_id = "fake.entity" attribute = "fake_attribute" - await setup_knx_integration( - hass, - knx_ip_interface_mock, + await knx.setup_integration( { CONF_KNX_EXPOSE: { CONF_TYPE: "percentU8", @@ -66,26 +47,76 @@ async def test_expose_attribute(hass, knx_ip_interface_mock): ) assert not hass.states.async_all() - # Change state to on; no attribute - knx_ip_interface_mock.reset_mock() + # Before init no response shall be sent + await knx.receive_read("1/1/8") + await knx.assert_telegram_count(0) + + # Change state to "on"; no attribute hass.states.async_set(entity_id, "on", {}) - await hass.async_block_till_done() - assert knx_ip_interface_mock.send_telegram.call_count == 0 + await knx.assert_telegram_count(0) # Change attribute; keep state - knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {attribute: 1}) - await hass.async_block_till_done() - assert knx_ip_interface_mock.send_telegram.call_count == 1 + await knx.assert_write("1/1/8", (1,)) + + # Read in between + await knx.receive_read("1/1/8") + await knx.assert_response("1/1/8", (1,)) # Change state keep attribute - knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "off", {attribute: 1}) - await hass.async_block_till_done() - assert knx_ip_interface_mock.send_telegram.call_count == 0 + await knx.assert_telegram_count(0) # Change state and attribute - knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {attribute: 0}) - await hass.async_block_till_done() - assert knx_ip_interface_mock.send_telegram.call_count == 1 + await knx.assert_write("1/1/8", (0,)) + + # Change state to "off"; no attribute + hass.states.async_set(entity_id, "off", {}) + await knx.assert_telegram_count(0) + + +async def test_expose_attribute_with_default(hass, knx): + """Test an expose to only send telegrams on attribute change.""" + entity_id = "fake.entity" + attribute = "fake_attribute" + await knx.setup_integration( + { + CONF_KNX_EXPOSE: { + CONF_TYPE: "percentU8", + KNX_ADDRESS: "1/1/8", + CONF_ENTITY_ID: entity_id, + CONF_ATTRIBUTE: attribute, + ExposeSchema.CONF_KNX_EXPOSE_DEFAULT: 0, + } + }, + ) + assert not hass.states.async_all() + + # Before init default value shall be sent as response + await knx.receive_read("1/1/8") + await knx.assert_response("1/1/8", (0,)) + + # Change state to "on"; no attribute + hass.states.async_set(entity_id, "on", {}) + await knx.assert_write("1/1/8", (0,)) + + # Change attribute; keep state + hass.states.async_set(entity_id, "on", {attribute: 1}) + await knx.assert_write("1/1/8", (1,)) + + # Change state keep attribute + hass.states.async_set(entity_id, "off", {attribute: 1}) + await knx.assert_no_telegram() + + # Change state and attribute + hass.states.async_set(entity_id, "on", {attribute: 3}) + await knx.assert_write("1/1/8", (3,)) + + # Read in between + await knx.receive_read("1/1/8") + await knx.assert_response("1/1/8", (3,)) + + # Change state to "off"; no attribute + hass.states.async_set(entity_id, "off", {}) + await knx.assert_write("1/1/8", (0,)) From 18bc2f95c8d4f9e7644dc5ea65789176d652e5a7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 20 Jul 2021 06:41:30 +0200 Subject: [PATCH 392/818] Small log addition for samsungtv (#53206) --- homeassistant/components/samsungtv/bridge.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 1cdd63acd3c..095d3339428 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -209,8 +209,8 @@ class SamsungTVLegacyBridge(SamsungTVBridge): except AccessDenied: LOGGER.debug("Working but denied config: %s", config) return RESULT_AUTH_MISSING - except UnhandledResponse: - LOGGER.debug("Working but unsupported config: %s", config) + except UnhandledResponse as err: + LOGGER.debug("Working but unsupported config: %s, error: %s", config, err) return RESULT_NOT_SUPPORTED except (ConnectionClosed, OSError) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) @@ -289,8 +289,10 @@ class SamsungTVWSBridge(SamsungTVBridge): config[CONF_TOKEN] = "*****" LOGGER.debug("Working config: %s", config) return RESULT_SUCCESS - except WebSocketException: - LOGGER.debug("Working but unsupported config: %s", config) + except WebSocketException as err: + LOGGER.debug( + "Working but unsupported config: %s, error: %s", config, err + ) result = RESULT_NOT_SUPPORTED except (OSError, ConnectionFailure) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) From 19a282255b19a110aafe2c3b46608ea11906455e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 06:52:58 +0200 Subject: [PATCH 393/818] Remove duplicate functions in modbus climate/sensor. (#53141) Convert all data types correctly for climate. --- .../components/modbus/base_platform.py | 81 ++++++++++++++++++ homeassistant/components/modbus/climate.py | 52 +----------- homeassistant/components/modbus/sensor.py | 85 ++----------------- 3 files changed, 90 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index c382923e15d..39ec283519a 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -4,17 +4,21 @@ from __future__ import annotations from abc import abstractmethod from datetime import timedelta import logging +import struct from typing import Any from homeassistant.const import ( CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, + CONF_COUNT, CONF_DELAY, CONF_DEVICE_CLASS, CONF_NAME, + CONF_OFFSET, CONF_SCAN_INTERVAL, CONF_SLAVE, + CONF_STRUCTURE, STATE_ON, ) from homeassistant.helpers.entity import Entity @@ -30,11 +34,19 @@ from .const import ( CALL_TYPE_WRITE_REGISTERS, CALL_TYPE_X_COILS, CALL_TYPE_X_REGISTER_HOLDINGS, + CONF_DATA_TYPE, CONF_INPUT_TYPE, + CONF_PRECISION, + CONF_SCALE, CONF_STATE_OFF, CONF_STATE_ON, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, CONF_VERIFY, CONF_WRITE_TYPE, + DATA_TYPE_STRING, ) from .modbus import ModbusHub @@ -90,6 +102,75 @@ class BasePlatform(Entity): return self._available +class BaseStructPlatform(BasePlatform, RestoreEntity): + """Base class representing a sensor/climate.""" + + def __init__(self, hub: ModbusHub, config: dict) -> None: + """Initialize the switch.""" + super().__init__(hub, config) + self._swap = config[CONF_SWAP] + self._data_type = config[CONF_DATA_TYPE] + self._structure = config.get(CONF_STRUCTURE) + self._precision = config[CONF_PRECISION] + self._scale = config[CONF_SCALE] + self._offset = config[CONF_OFFSET] + self._count = config[CONF_COUNT] + + def _swap_registers(self, registers): + """Do swap as needed.""" + if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: + # convert [12][34] --> [21][43] + for i, register in enumerate(registers): + registers[i] = int.from_bytes( + register.to_bytes(2, byteorder="little"), + byteorder="big", + signed=False, + ) + if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: + # convert [12][34] ==> [34][12] + registers.reverse() + return registers + + def unpack_structure_result(self, registers): + """Convert registers to proper result.""" + + registers = self._swap_registers(registers) + byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) + if self._data_type == DATA_TYPE_STRING: + self._value = byte_string.decode() + else: + val = struct.unpack(self._structure, byte_string) + + # Issue: https://github.com/home-assistant/core/issues/41944 + # If unpack() returns a tuple greater than 1, don't try to process the value. + # Instead, return the values of unpack(...) separated by commas. + if len(val) > 1: + # Apply scale and precision to floats and ints + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) + else: + v_result.append(f"{float(v_temp):.{self._precision}f}") + self._value = ",".join(map(str, v_result)) + else: + # Apply scale and precision to floats and ints + val = self._scale * val[0] + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: + self._value = str(val) + else: + self._value = f"{float(val):.{self._precision}f}" + + class BaseSwitch(BasePlatform, RestoreEntity): """Base class representing a Modbus switch.""" diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index dbec27c3af6..75a1f846c76 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -11,9 +11,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - CONF_COUNT, CONF_NAME, - CONF_OFFSET, CONF_STRUCTURE, CONF_TEMPERATURE_UNIT, PRECISION_TENTHS, @@ -25,22 +23,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .base_platform import BasePlatform +from .base_platform import BaseStructPlatform from .const import ( ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, - CONF_DATA_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, - CONF_PRECISION, - CONF_SCALE, CONF_STEP, - CONF_SWAP, - CONF_SWAP_BYTE, - CONF_SWAP_WORD, - CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, @@ -69,7 +60,7 @@ async def async_setup_platform( async_add_entities(entities) -class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): +class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Representation of a Modbus Thermostat.""" def __init__( @@ -82,17 +73,11 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._target_temperature_register = config[CONF_TARGET_TEMP] self._target_temperature = None self._current_temperature = None - self._data_type = config[CONF_DATA_TYPE] self._structure = config[CONF_STRUCTURE] - self._count = config[CONF_COUNT] - self._precision = config[CONF_PRECISION] - self._scale = config[CONF_SCALE] - self._offset = config[CONF_OFFSET] self._unit = config[CONF_TEMPERATURE_UNIT] self._max_temp = config[CONF_MAX_TEMP] self._min_temp = config[CONF_MIN_TEMP] self._temp_step = config[CONF_STEP] - self._swap = config[CONF_SWAP] async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -175,21 +160,6 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._available = result is not None await self.async_update() - def _swap_registers(self, registers): - """Do swap as needed.""" - if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: - # convert [12][34] --> [21][43] - for i, register in enumerate(registers): - registers[i] = int.from_bytes( - register.to_bytes(2, byteorder="little"), - byteorder="big", - signed=False, - ) - if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: - # convert [12][34] ==> [34][12] - registers.reverse() - return registers - async def async_update(self, now=None): """Update Target & Current Temperature.""" # remark "now" is a dummy parameter to avoid problems with @@ -217,21 +187,7 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._available = False return -1 - registers = self._swap_registers(result.registers) - byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) - val = struct.unpack(self._structure, byte_string) - if len(val) != 1 or not isinstance(val[0], (float, int)): - _LOGGER.error( - "Unable to parse result as a single int or float value; adjust your configuration. Result: %s", - str(val), - ) - return -1 + self.unpack_structure_result(result.registers) - val2 = val[0] - register_value = format( - (self._scale * val2) + self._offset, f".{self._precision}f" - ) - register_value2 = float(register_value) self._available = True - - return register_value2 + return self._value diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 9f1e7572a58..1f2b5e32f6f 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,34 +2,16 @@ from __future__ import annotations import logging -import struct from typing import Any from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - CONF_COUNT, - CONF_NAME, - CONF_OFFSET, - CONF_SENSORS, - CONF_STRUCTURE, - CONF_UNIT_OF_MEASUREMENT, -) +from homeassistant.const import CONF_NAME, CONF_SENSORS, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .base_platform import BasePlatform -from .const import ( - CONF_DATA_TYPE, - CONF_PRECISION, - CONF_SCALE, - CONF_SWAP, - CONF_SWAP_BYTE, - CONF_SWAP_WORD, - CONF_SWAP_WORD_BYTE, - DATA_TYPE_STRING, - MODBUS_DOMAIN, -) +from .base_platform import BaseStructPlatform +from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -55,7 +37,7 @@ async def async_setup_platform( async_add_entities(sensors) -class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): +class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): """Modbus register sensor.""" def __init__( @@ -66,13 +48,6 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): """Initialize the modbus register sensor.""" super().__init__(hub, entry) self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) - self._count = int(entry[CONF_COUNT]) - self._swap = entry[CONF_SWAP] - self._scale = entry[CONF_SCALE] - self._offset = entry[CONF_OFFSET] - self._precision = entry[CONF_PRECISION] - self._structure = entry.get(CONF_STRUCTURE) - self._data_type = entry[CONF_DATA_TYPE] async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -91,21 +66,6 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): """Return the unit of measurement.""" return self._unit_of_measurement - def _swap_registers(self, registers): - """Do swap as needed.""" - if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: - # convert [12][34] --> [21][43] - for i, register in enumerate(registers): - registers[i] = int.from_bytes( - register.to_bytes(2, byteorder="little"), - byteorder="big", - signed=False, - ) - if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: - # convert [12][34] ==> [34][12] - registers.reverse() - return registers - async def async_update(self, now=None): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with @@ -118,41 +78,6 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() return - registers = self._swap_registers(result.registers) - byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) - if self._data_type == DATA_TYPE_STRING: - self._value = byte_string.decode() - else: - val = struct.unpack(self._structure, byte_string) - - # Issue: https://github.com/home-assistant/core/issues/41944 - # If unpack() returns a tuple greater than 1, don't try to process the value. - # Instead, return the values of unpack(...) separated by commas. - if len(val) > 1: - # Apply scale and precision to floats and ints - v_result = [] - for entry in val: - v_temp = self._scale * entry + self._offset - - # We could convert int to float, and the code would still work; however - # we lose some precision, and unit tests will fail. Therefore, we do - # the conversion only when it's absolutely necessary. - if isinstance(v_temp, int) and self._precision == 0: - v_result.append(str(v_temp)) - else: - v_result.append(f"{float(v_temp):.{self._precision}f}") - self._value = ",".join(map(str, v_result)) - else: - # Apply scale and precision to floats and ints - val = self._scale * val[0] + self._offset - - # We could convert int to float, and the code would still work; however - # we lose some precision, and unit tests will fail. Therefore, we do - # the conversion only when it's absolutely necessary. - if isinstance(val, int) and self._precision == 0: - self._value = str(val) - else: - self._value = f"{float(val):.{self._precision}f}" - + self.unpack_structure_result(result.registers) self._available = True self.async_write_ha_state() From 78a8ba99f9d4aff5e27b173304d5865417347327 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 06:57:28 +0200 Subject: [PATCH 394/818] Upgrade modbus to quality level "silver". (#53186) --- homeassistant/components/modbus/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index f5c7bf2df4e..9f2208de175 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/modbus", "requirements": ["pymodbus==2.5.2"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], + "quality_scale": "silver", "iot_class": "local_polling" } From c2a2f503162bbf580f0b3902e957ac9514088f41 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 19 Jul 2021 22:59:31 -0700 Subject: [PATCH 395/818] mypy cleanup for homeassistant.components.nest (#53214) --- homeassistant/components/nest/__init__.py | 1 - homeassistant/components/nest/config_flow.py | 23 ++++++++++--------- .../components/nest/device_trigger.py | 2 +- homeassistant/components/nest/sensor_sdm.py | 2 +- mypy.ini | 2 +- script/hassfest/mypy_config.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fb488763750..1548814804b 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -33,7 +33,6 @@ from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_T from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) CONF_PROJECT_ID = "project_id" diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index c6ebe543c99..2070f232d77 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -22,6 +22,7 @@ import async_timeout import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.json import load_json @@ -79,13 +80,13 @@ class NestFlowHandler( self._reauth = False @classmethod - def register_sdm_api(cls, hass): + def register_sdm_api(cls, hass) -> None: """Configure the flow handler to use the SDM API.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_SDM] = {} - def is_sdm_api(self): + def is_sdm_api(self) -> bool: """Return true if this flow is setup to use SDM API.""" return DOMAIN in self.hass.data and DATA_SDM in self.hass.data[DOMAIN] @@ -104,7 +105,7 @@ class NestFlowHandler( "prompt": "consent", } - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict) -> FlowResult: """Create an entry for the SDM flow.""" assert self.is_sdm_api(), "Step only supported for SDM API" data[DATA_SDM] = {} @@ -128,13 +129,13 @@ class NestFlowHandler( return await super().async_oauth_create_entry(data) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, user_input=None) -> FlowResult: """Perform reauth upon an API authentication error.""" assert self.is_sdm_api(), "Step only supported for SDM API" self._reauth = True # Forces update of existing config entry return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm(self, user_input=None) -> FlowResult: """Confirm reauth dialog.""" assert self.is_sdm_api(), "Step only supported for SDM API" if user_input is None: @@ -144,7 +145,7 @@ class NestFlowHandler( ) return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle a flow initialized by the user.""" if self.is_sdm_api(): # Reauth will update an existing entry @@ -153,7 +154,7 @@ class NestFlowHandler( return await super().async_step_user(user_input) return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Handle a flow start.""" assert not self.is_sdm_api(), "Step only supported for legacy API" @@ -178,7 +179,7 @@ class NestFlowHandler( data_schema=vol.Schema({vol.Required("flow_impl"): vol.In(list(flows))}), ) - async def async_step_link(self, user_input=None): + async def async_step_link(self, user_input=None) -> FlowResult: """Attempt to link with the Nest account. Route the user to a website to authenticate with Nest. Depending on @@ -225,7 +226,7 @@ class NestFlowHandler( errors=errors, ) - async def async_step_import(self, info): + async def async_step_import(self, info) -> FlowResult: """Import existing auth from Nest.""" assert not self.is_sdm_api(), "Step only supported for legacy API" @@ -235,7 +236,7 @@ class NestFlowHandler( config_path = info["nest_conf_path"] if not await self.hass.async_add_executor_job(os.path.isfile, config_path): - self.flow_impl = DOMAIN + self.flow_impl = DOMAIN # type: ignore return await self.async_step_link() flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] @@ -246,7 +247,7 @@ class NestFlowHandler( ) @callback - def _entry_from_tokens(self, title, flow, tokens): + def _entry_from_tokens(self, title, flow, tokens) -> FlowResult: """Create an entry from tokens.""" return self.async_create_entry( title=title, data={"tokens": tokens, "impl_domain": flow["domain"]} diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 4ed492a15fa..889111d6f61 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -27,7 +27,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str: +async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: """Get the nest API device_id from the HomeAssistant device_id.""" device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(device_id) diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 8182ef3ed95..d1ff26880de 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -44,7 +44,7 @@ async def async_setup_sdm_entry( _LOGGER.warning("Failed to get devices: %s", err) raise PlatformNotReady from err - entities = [] + entities: list[SensorEntity] = [] for device in device_manager.devices.values(): if TemperatureTrait.NAME in device.traits: entities.append(TemperatureSensor(device)) diff --git a/mypy.ini b/mypy.ini index 90880107761..8354b145a2f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1364,7 +1364,7 @@ ignore_errors = true [mypy-homeassistant.components.ness_alarm.*] ignore_errors = true -[mypy-homeassistant.components.nest.*] +[mypy-homeassistant.components.nest.legacy.*] ignore_errors = true [mypy-homeassistant.components.netatmo.*] diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f47966bdb5c..3be6471fa7f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -113,7 +113,7 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.mullvad.*", "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", - "homeassistant.components.nest.*", + "homeassistant.components.nest.legacy.*", "homeassistant.components.netatmo.*", "homeassistant.components.netio.*", "homeassistant.components.nightscout.*", From 7659197154f767847f6423fb54ebc3d65c2c029c Mon Sep 17 00:00:00 2001 From: Kyle Niewiada Date: Tue, 20 Jul 2021 02:15:11 -0400 Subject: [PATCH 396/818] Increase interval to stop Connection reset by peer (#53202) --- homeassistant/components/mutesync/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py index 5e288b405af..027a48f46ca 100644 --- a/homeassistant/components/mutesync/const.py +++ b/homeassistant/components/mutesync/const.py @@ -5,4 +5,4 @@ from typing import Final DOMAIN: Final = "mutesync" UPDATE_INTERVAL_NOT_IN_MEETING: Final = timedelta(seconds=10) -UPDATE_INTERVAL_IN_MEETING: Final = timedelta(seconds=5) +UPDATE_INTERVAL_IN_MEETING: Final = timedelta(seconds=10) From c9ae141eab1bc03ac42b300b9931adb0ec06749a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 20 Jul 2021 02:33:45 -0400 Subject: [PATCH 397/818] Mark ZHA Light methods as a callbacks (#53170) * ZHA Light.set_level is safe to run in the loop * Fix tests. --- homeassistant/components/zha/light.py | 1 + tests/components/zha/test_light.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index ca84a579d14..628d9c3b9be 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -167,6 +167,7 @@ class BaseLight(LogMixin, light.LightEntity): """Return the warmest color_temp that this light supports.""" return self._max_mireds + @callback def set_level(self, value): """Set the brightness of this light between 0..254. diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index fe367a3969b..0408f164049 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -421,7 +421,7 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected await send_attributes_report( hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} ) - await hass.async_block_till_done() + await async_wait_for_updates(hass) assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: From e453871c689306170582db784939fb24b978ca6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jul 2021 09:37:36 +0300 Subject: [PATCH 398/818] Bump codecov/codecov-action from 1.5.2 to 2.0.1 (#53216) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1.5.2 to 2.0.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.5.2...v2.0.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0809cf604cf..8e03a527e74 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -740,4 +740,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.5.2 + uses: codecov/codecov-action@v2.0.1 From ab36ac7a94890b7bf8b7ab2811c0ad89f1a814d8 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 20 Jul 2021 00:20:47 -0700 Subject: [PATCH 399/818] Handle all WeMo ensure_long_press_virtual_device exceptions (#53094) * Handle all exceptions around the WeMo ensure_long_press_virtual_device method * Don't use a bare exception Co-authored-by: Martin Hjelmare * Log exception Co-authored-by: Martin Hjelmare --- homeassistant/components/wemo/wemo_device.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 3b0fbdcbe55..6fd1f4d5512 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -1,7 +1,7 @@ """Home Assistant wrapper for a pyWeMo device.""" import logging -from pywemo import PyWeMoException, WeMoDevice +from pywemo import WeMoDevice from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from homeassistant.config_entries import ConfigEntry @@ -81,8 +81,10 @@ async def async_register_device( if device.supports_long_press: try: await hass.async_add_executor_job(wemo.ensure_long_press_virtual_device) - except PyWeMoException: - _LOGGER.warning( + # Temporarily handling all exceptions for #52996 & pywemo/pywemo/issues/276 + # Replace this with `except: PyWeMoException` after upstream has been fixed. + except Exception: # pylint: disable=broad-except + _LOGGER.exception( "Failed to enable long press support for device: %s", wemo.name ) device.supports_long_press = False From e7ccd1a5494fc9c69c90676b1683e9b24ca24ed5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 09:53:24 +0200 Subject: [PATCH 400/818] Correct typing and activate mypy. (#53217) --- homeassistant/components/huisbaasje/sensor.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 038325ece4a..3cda3cdec00 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -65,7 +65,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): return self._name @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class diff --git a/mypy.ini b/mypy.ini index 8354b145a2f..9f37f46d713 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1253,9 +1253,6 @@ ignore_errors = true [mypy-homeassistant.components.honeywell.*] ignore_errors = true -[mypy-homeassistant.components.huisbaasje.*] -ignore_errors = true - [mypy-homeassistant.components.humidifier.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3be6471fa7f..666575a86ed 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -76,7 +76,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.homekit_controller.*", "homeassistant.components.homematicip_cloud.*", "homeassistant.components.honeywell.*", - "homeassistant.components.huisbaasje.*", "homeassistant.components.humidifier.*", "homeassistant.components.iaqualink.*", "homeassistant.components.icloud.*", From 05fa220703c16cf42a8b05b972101b6c32f69b34 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 20 Jul 2021 04:31:12 -0400 Subject: [PATCH 401/818] Add support for options in zwave_js.set_value service (#53212) --- homeassistant/components/zwave_js/const.py | 1 + homeassistant/components/zwave_js/services.py | 3 ++ .../components/zwave_js/services.yaml | 6 +++ tests/components/zwave_js/test_services.py | 42 +++++++++++++++++++ .../zwave_js/aeon_smart_switch_6_state.json | 3 +- 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index d1b9ecaaa15..4687110e208 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -43,6 +43,7 @@ ATTR_EVENT_TYPE = "event_type" ATTR_EVENT_DATA = "event_data" ATTR_DATA_TYPE = "data_type" ATTR_WAIT_FOR_RESULT = "wait_for_result" +ATTR_OPTIONS = "options" ATTR_NODE = "node" ATTR_ZWAVE_VALUE = "zwave_value" diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 00f991433f4..e09b0fb0c5e 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -254,6 +254,7 @@ class ZWaveServices: vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean, + vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA}, }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), get_nodes_from_service_data, @@ -381,6 +382,7 @@ class ZWaveServices: endpoint = service.data.get(const.ATTR_ENDPOINT) new_value = service.data[const.ATTR_VALUE] wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT) + options = service.data.get(const.ATTR_OPTIONS) for node in nodes: success = await node.async_set_value( @@ -392,6 +394,7 @@ class ZWaveServices: property_key=property_key, ), new_value, + options=options, wait_for_result=wait_for_result, ) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index c24fa4694cf..7d88f5f7830 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -154,6 +154,12 @@ set_value: required: true selector: object: + options: + name: Options + description: Set value options. Refer to the Z-Wave JS documentation for more information on what options can be set. + required: false + selector: + object: wait_for_result: name: Wait for result? description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device. diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 25373bcb026..92ead72e2ad 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -11,6 +11,7 @@ from homeassistant.components.zwave_js.const import ( ATTR_CONFIG_PARAMETER, ATTR_CONFIG_PARAMETER_BITMASK, ATTR_CONFIG_VALUE, + ATTR_OPTIONS, ATTR_PROPERTY, ATTR_REFRESH_ALL_VALUES, ATTR_VALUE, @@ -31,6 +32,7 @@ from homeassistant.helpers.device_registry import ( from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .common import ( + AEON_SMART_SWITCH_LIGHT_ENTITY, AIR_TEMPERATURE_SENSOR, CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY, @@ -759,6 +761,46 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) +async def test_set_value_options(hass, client, aeon_smart_switch_6, integration): + """Test set_value service with options.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: AEON_SMART_SWITCH_LIGHT_ENTITY, + ATTR_COMMAND_CLASS: 37, + ATTR_PROPERTY: "targetValue", + ATTR_VALUE: 2, + ATTR_OPTIONS: {"transitionDuration": 1}, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == aeon_smart_switch_6.node_id + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == 2 + assert args["options"] == {"transitionDuration": 1} + + client.async_send_command.reset_mock() + + async def test_multicast_set_value( hass, client, diff --git a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json index 36db78faace..c8d8f878c0b 100644 --- a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json +++ b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json @@ -81,7 +81,8 @@ "type": "boolean", "readable": true, "writeable": true, - "label": "Target value" + "label": "Target value", + "valueChangeOptions": ["transitionDuration"] } }, { From cd37c2492b00b536d0b799ed61ffbd056123d66a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 20 Jul 2021 05:13:59 -0400 Subject: [PATCH 402/818] Use entity class attributes for acer_projector (#52432) Co-authored-by: Shay Levy --- .../components/acer_projector/switch.py | 46 +++++-------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 69aba415589..947b774b7bd 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -67,6 +67,8 @@ def setup_platform( class AcerSwitch(SwitchEntity): """Represents an Acer Projector as a switch.""" + _attr_icon = ICON + def __init__( self, serial_port: str, @@ -79,9 +81,7 @@ class AcerSwitch(SwitchEntity): port=serial_port, timeout=timeout, write_timeout=write_timeout ) self._serial_port = serial_port - self._name = name - self._state = False - self._available = False + self._attr_name = name self._attributes = { LAMP_HOURS: STATE_UNKNOWN, INPUT_SOURCE: STATE_UNKNOWN, @@ -116,57 +116,33 @@ class AcerSwitch(SwitchEntity): return match.group(1) return STATE_UNKNOWN - @property - def available(self) -> bool: - """Return if projector is available.""" - return self._available - - @property - def name(self) -> str: - """Return name of the projector.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon.""" - return ICON - - @property - def is_on(self) -> bool: - """Return if the projector is turned on.""" - return self._state - - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return state attributes.""" - return self._attributes - def update(self) -> None: """Get the latest state from the projector.""" awns = self._write_read_format(CMD_DICT[LAMP]) if awns == "Lamp 1": - self._state = True - self._available = True + self._attr_is_on = True + self._attr_available = True elif awns == "Lamp 0": - self._state = False - self._available = True + self._attr_is_on = False + self._attr_available = True else: - self._available = False + self._attr_available = False for key in self._attributes: msg = CMD_DICT.get(key) if msg: awns = self._write_read_format(msg) self._attributes[key] = awns + self._attr_extra_state_attributes = self._attributes def turn_on(self, **kwargs: Any) -> None: """Turn the projector on.""" msg = CMD_DICT[STATE_ON] self._write_read(msg) - self._state = True + self._attr_is_on = True def turn_off(self, **kwargs: Any) -> None: """Turn the projector off.""" msg = CMD_DICT[STATE_OFF] self._write_read(msg) - self._state = False + self._attr_is_on = False From 5d2ce19746d6c49b05267dc06efb733969001e8b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Jul 2021 11:31:48 +0200 Subject: [PATCH 403/818] Update python-typing-update to v0.3.5 (#53223) * Update python-typing-update to 0.3.5 * Update typing --- .pre-commit-config.yaml | 2 +- homeassistant/components/search/__init__.py | 4 +++- tests/components/litterrobot/conftest.py | 12 +++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a2e6f45fdd..fa3350b36d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,7 +70,7 @@ repos: - id: prettier stages: [manual] - repo: https://github.com/cdce8p/python-typing-update - rev: v0.3.3 + rev: v0.3.5 hooks: # Run `python-typing-update` hook manually from time to time # to update python typing syntax. diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 93da95bc550..fc13b8ca098 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -1,4 +1,6 @@ """The Search integration.""" +from __future__ import annotations + from collections import defaultdict, deque import logging @@ -71,7 +73,7 @@ class Searcher: hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: entity_registry.EntityRegistry, - entity_sources: "dict[str, dict[str, str]]", + entity_sources: dict[str, dict[str, str]], ) -> None: """Search results.""" self.hass = hass diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 237317545a1..c408ed28819 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -1,5 +1,7 @@ """Configure pytest for Litter-Robot tests.""" -from typing import Any, Optional +from __future__ import annotations + +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from pylitterbot import Account, Robot @@ -15,7 +17,7 @@ from tests.common import MockConfigEntry def create_mock_robot( - robot_data: Optional[dict] = None, side_effect: Optional[Any] = None + robot_data: dict | None = None, side_effect: Any | None = None ) -> Robot: """Create a mock Litter-Robot device.""" if not robot_data: @@ -33,8 +35,8 @@ def create_mock_robot( def create_mock_account( - robot_data: Optional[dict] = None, - side_effect: Optional[Any] = None, + robot_data: dict | None = None, + side_effect: Any | None = None, skip_robots: bool = False, ) -> MagicMock: """Create a mock Litter-Robot account.""" @@ -72,7 +74,7 @@ def mock_account_with_side_effects() -> MagicMock: async def setup_integration( - hass: HomeAssistant, mock_account: MagicMock, platform_domain: Optional[str] = None + hass: HomeAssistant, mock_account: MagicMock, platform_domain: str | None = None ) -> MockConfigEntry: """Load a Litter-Robot platform with the provided hub.""" entry = MockConfigEntry( From 79a418f1bcd5fef180ff67745f5dc4e74e4d876c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 20 Jul 2021 06:20:56 -0400 Subject: [PATCH 404/818] Use entity class attributes for Brottsplatskartan (#53163) --- .../components/brottsplatskartan/sensor.py | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 32af96dfe60..c327f9122ce 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -82,25 +82,8 @@ class BrottsplatskartanSensor(SensorEntity): def __init__(self, bpk, name): """Initialize the Brottsplatskartan sensor.""" - self._attributes = {} self._brottsplatskartan = bpk - self._name = name - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes + self._attr_name = name def update(self): """Update device state.""" @@ -116,6 +99,8 @@ class BrottsplatskartanSensor(SensorEntity): incident_type = incident.get("title_type") incident_counts[incident_type] += 1 - self._attributes = {ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION} - self._attributes.update(incident_counts) - self._state = len(incidents) + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION + } + self._attr_extra_state_attributes.update(incident_counts) + self._attr_state = len(incidents) From d17776af87da3a5c533d6dcdaee2ccf092d78152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 12:28:50 +0200 Subject: [PATCH 405/818] Tibber, accumulated reward (#53195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tibber/sensor.py | 10 +++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index a915db8a665..20b62832619 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.18.0"], + "requirements": ["pyTibber==0.19.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index bf6218dcb31..8a296fd93b2 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -129,6 +129,12 @@ RT_SENSOR_MAP = { SIGNAL_STRENGTH_DECIBELS, STATE_CLASS_MEASUREMENT, ], + "accumulatedReward": [ + "accumulated reward", + DEVICE_CLASS_MONETARY, + None, + STATE_CLASS_MEASUREMENT, + ], "accumulatedCost": [ "accumulated cost", DEVICE_CLASS_MONETARY, @@ -330,6 +336,7 @@ class TibberSensorRT(TibberSensor): "accumulated consumption", "accumulated production", "accumulated cost", + "accumulated reward", ]: self._attr_last_reset = dt_util.as_utc( dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) @@ -366,6 +373,7 @@ class TibberSensorRT(TibberSensor): "accumulated consumption", "accumulated production", "accumulated cost", + "accumulated reward", ]: self._attr_last_reset = dt_util.as_utc( timestamp.replace(hour=0, minute=0, second=0, microsecond=0) @@ -422,7 +430,7 @@ class TibberRtDataHandler: sensor_name, device_class, unit, state_class = RT_SENSOR_MAP[ sensor_type ] - if sensor_type == "accumulatedCost": + if sensor_type in ["accumulatedCost", "accumulatedReward"]: unit = self._tibber_home.currency entity = TibberSensorRT( self._tibber_home, diff --git a/requirements_all.txt b/requirements_all.txt index d52b4ca5414..1a79b9d76d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1271,7 +1271,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.18.0 +pyTibber==0.19.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f02e611debf..04835c3d369 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -714,7 +714,7 @@ pyMetno==0.8.3 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.18.0 +pyTibber==0.19.0 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From 51dd95ce353009400a22cb038d2b3a70ebebada5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 13:00:07 +0200 Subject: [PATCH 406/818] Review comments on earlier merge. (#53221) --- homeassistant/components/azure_devops/__init__.py | 2 +- homeassistant/components/azure_devops/sensor.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index d8b326bf2d5..39307ac41df 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -72,7 +72,7 @@ class AzureDevOpsEntity(Entity): ) self._attr_available = False - async def _azure_devops_update(self) -> None: + async def _azure_devops_update(self) -> bool: """Update Azure DevOps entity.""" raise NotImplementedError() diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index ee3a0356a52..d7589cf5014 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -97,7 +97,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): "mdi:pipe", ) - async def _azure_devops_update(self) -> None: + async def _azure_devops_update(self) -> bool: """Update Azure DevOps entity.""" try: build: DevOpsBuild = await self.client.get_build( @@ -106,7 +106,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): except aiohttp.ClientError as exception: _LOGGER.warning(exception) self._attr_available = False - return + return False self._attr_state = build.build_number self._attr_extra_state_attributes = { "definition_id": build.definition.id, @@ -123,3 +123,4 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): "finish_time": build.finish_time, } self._attr_available = True + return True From b4a50f5459a12edaedde93c05882978606199836 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 20 Jul 2021 13:56:23 +0200 Subject: [PATCH 407/818] Add unique ID support to light, cover and media player groups (#53225) --- homeassistant/components/group/cover.py | 15 ++++++++++++--- homeassistant/components/group/light.py | 11 +++++++++-- homeassistant/components/group/media_player.py | 15 ++++++++++++--- tests/components/group/test_cover.py | 8 ++++++++ tests/components/group/test_light.py | 9 ++++++++- tests/components/group/test_media_player.py | 7 +++++++ 6 files changed, 56 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index b5022582d9e..397c7e609f3 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -36,6 +36,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, CONF_NAME, + CONF_UNIQUE_ID, STATE_CLOSING, STATE_OPEN, STATE_OPENING, @@ -57,8 +58,9 @@ DEFAULT_NAME = "Cover Group" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -70,7 +72,13 @@ async def async_setup_platform( discovery_info: dict[str, Any] | None = None, ) -> None: """Set up the Group Cover platform.""" - async_add_entities([CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + async_add_entities( + [ + CoverGroup( + config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] + ) + ] + ) class CoverGroup(GroupEntity, CoverEntity): @@ -82,7 +90,7 @@ class CoverGroup(GroupEntity, CoverEntity): _attr_current_cover_position: int | None = 100 _attr_assumed_state: bool = True - def __init__(self, name: str, entities: list[str]) -> None: + def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a CoverGroup entity.""" self._entities = entities self._covers: dict[str, set[str]] = { @@ -98,6 +106,7 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} + self._attr_unique_id = unique_id async def _update_supported_features_event(self, event: Event) -> None: self.async_set_context(event.context) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 3f5a6eaf13e..bb0762d2278 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, CONF_NAME, + CONF_UNIQUE_ID, STATE_ON, STATE_UNAVAILABLE, ) @@ -55,6 +56,7 @@ DEFAULT_NAME = "Light Group" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN), } ) @@ -72,7 +74,11 @@ async def async_setup_platform( ) -> None: """Initialize light.group platform.""" async_add_entities( - [LightGroup(cast(str, config.get(CONF_NAME)), config[CONF_ENTITIES])] + [ + LightGroup( + config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] + ) + ] ) @@ -86,13 +92,14 @@ class LightGroup(GroupEntity, light.LightEntity): _attr_min_mireds = 154 _attr_should_poll = False - def __init__(self, name: str, entity_ids: list[str]) -> None: + def __init__(self, unique_id: str | None, name: str, entity_ids: list[str]) -> None: """Initialize a light group.""" self._entity_ids = entity_ids self._white_value: int | None = None self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 568812fd6e0..810959609b5 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -48,6 +48,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, CONF_NAME, + CONF_UNIQUE_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -71,8 +72,9 @@ DEFAULT_NAME = "Media Group" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -84,17 +86,24 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Media Group platform.""" - async_add_entities([MediaGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + async_add_entities( + [ + MediaGroup( + config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] + ) + ] + ) class MediaGroup(MediaPlayerEntity): """Representation of a Media Group.""" - def __init__(self, name: str, entities: list[str]) -> None: + def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a Media Group entity.""" self._name = name self._state: str | None = None self._supported_features: int = 0 + self._attr_unique_id = unique_id self._entities = entities self._features: dict[str, set[str]] = { diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 59bde36b46b..8a29274298b 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, + CONF_UNIQUE_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, @@ -32,6 +33,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -77,6 +79,7 @@ CONFIG_ATTRIBUTES = { DOMAIN: { "platform": "group", CONF_ENTITIES: [DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT], + CONF_UNIQUE_ID: "unique_identifier", } } @@ -220,6 +223,11 @@ async def test_attributes(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.attributes[ATTR_ASSUMED_STATE] is True + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(COVER_GROUP) + assert entry + assert entry.unique_id == "unique_identifier" + @pytest.mark.parametrize("config_count", [(CONFIG_TILT_ONLY, 2)]) async def test_cover_that_only_supports_tilt_removed(hass, setup_comp): diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 06ad1b1101b..74275cf0bd2 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -44,6 +44,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -58,6 +59,7 @@ async def test_default_state(hass): "platform": DOMAIN, "entities": ["light.kitchen", "light.bedroom"], "name": "Bedroom Group", + "unique_id": "unique_identifier", } }, ) @@ -77,6 +79,11 @@ async def test_default_state(hass): assert state.attributes.get(ATTR_EFFECT_LIST) is None assert state.attributes.get(ATTR_EFFECT) is None + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("light.bedroom_group") + assert entry + assert entry.unique_id == "unique_identifier" + async def test_state_reporting(hass): """Test the state reporting.""" @@ -1064,7 +1071,7 @@ async def test_invalid_service_calls(hass): """Test invalid service call arguments get discarded.""" add_entities = MagicMock() await group.async_setup_platform( - hass, {"entities": ["light.test1", "light.test2"]}, add_entities + hass, {"name": "test", "entities": ["light.test1", "light.test2"]}, add_entities ) await hass.async_block_till_done() await hass.async_start() diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 5dd5e4225cc..27962297952 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -49,6 +49,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -73,6 +74,7 @@ async def test_default_state(hass): "platform": DOMAIN, "entities": ["media_player.player_1", "media_player.player_2"], "name": "Media group", + "unique_id": "unique_identifier", } }, ) @@ -89,6 +91,11 @@ async def test_default_state(hass): "media_player.player_2", ] + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("media_player.media_group") + assert entry + assert entry.unique_id == "unique_identifier" + async def test_state_reporting(hass): """Test the state reporting.""" From a56485a8c5b3164ca0f5d8f015dbfcbcb8811b4e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Jul 2021 14:13:51 +0200 Subject: [PATCH 408/818] Revert new unit types (#53226) --- homeassistant/components/bsblan/climate.py | 3 +- homeassistant/components/climate/__init__.py | 5 +- .../components/climate/device_trigger.py | 2 - .../components/devolo_home_control/climate.py | 4 +- homeassistant/components/esphome/climate.py | 3 +- homeassistant/components/fritzbox/climate.py | 3 +- homeassistant/components/lcn/climate.py | 3 +- homeassistant/components/mysensors/climate.py | 9 +- homeassistant/components/zwave_js/climate.py | 3 +- homeassistant/const.py | 280 ++++++++---------- homeassistant/helpers/temperature.py | 7 +- homeassistant/util/distance.py | 9 +- homeassistant/util/pressure.py | 7 +- homeassistant/util/temperature.py | 6 +- homeassistant/util/unit_system.py | 39 +-- homeassistant/util/volume.py | 5 +- 16 files changed, 153 insertions(+), 235 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 32473eabce1..3aa3679c6c9 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UnitTemperatureT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -122,7 +121,7 @@ class BSBLanClimate(ClimateEntity): return self._info.device_identification @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement which this thermostat uses.""" if self._temperature_unit == "°C": return TEMP_CELSIUS diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 0b80402b3ec..cc26bcc9bcc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -18,7 +18,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, TEMP_CELSIUS, - UnitTemperatureT, ) from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv @@ -196,7 +195,7 @@ class ClimateEntity(Entity): _attr_target_temperature_low: float | None _attr_target_temperature_step: float | None = None _attr_target_temperature: float | None = None - _attr_temperature_unit: UnitTemperatureT + _attr_temperature_unit: str @property def state(self) -> str: @@ -304,7 +303,7 @@ class ClimateEntity(Entity): return data @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" return self._attr_temperature_unit diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index db8eeedd54c..1b5127d7d4a 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -19,7 +19,6 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, PERCENTAGE, - UnitT, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry @@ -173,7 +172,6 @@ async def async_get_trigger_capabilities(hass: HomeAssistant, config): ) } - unit_of_measurement: UnitT if trigger_type == "current_temperature_changed": unit_of_measurement = hass.config.units.temperature_unit else: diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index cad439eb284..6b890544da5 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitTemperatureT +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,7 +105,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit return SUPPORT_TARGET_TEMPERATURE @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the supported unit of temperature.""" return TEMP_CELSIUS diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 016d197e8d9..218f0fb319b 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -65,7 +65,6 @@ from homeassistant.const import ( PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS, - UnitTemperatureT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -167,7 +166,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return PRECISION_TENTHS @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" return TEMP_CELSIUS diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 7f3ee76b48e..c50e0d4f270 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -23,7 +23,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PRECISION_HALVES, TEMP_CELSIUS, - UnitTemperatureT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -98,7 +97,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): return self.device.present # type: ignore [no-any-return] @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement that is used.""" return TEMP_CELSIUS diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index e453f26c25c..3bd8b551c49 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UnitTemperatureT, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -110,7 +109,7 @@ class LcnClimate(LcnEntity, ClimateEntity): return const.SUPPORT_TARGET_TEMPERATURE @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement.""" # Config schema only allows for: TEMP_CELSIUS and TEMP_FAHRENHEIT if self.unit == pypck.lcn_defs.VarUnit.FAHRENHEIT: diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 5afb4a803d2..5dd52673581 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -19,12 +19,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - UnitTemperatureT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -96,7 +91,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): return features @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 62da8cc8c80..1621e87cfab 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -53,7 +53,6 @@ from homeassistant.const import ( PRECISION_TENTHS, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UnitTemperatureT, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -249,7 +248,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore @property - def temperature_unit(self) -> UnitTemperatureT: + def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" if ( self._unit_value diff --git a/homeassistant/const.py b/homeassistant/const.py index 4e78e657ba6..6e2b7e17ce0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" from __future__ import annotations -from typing import Final, NewType +from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 @@ -395,216 +395,171 @@ ATTR_TEMPERATURE: Final = "temperature" # #### UNITS OF MEASUREMENT #### -UnitT = NewType("UnitT", str) - # Power units -UnitPowerT = NewType("UnitPowerT", UnitT) -POWER_WATT: Final[UnitPowerT] = UnitPowerT(UnitT("W")) -POWER_KILO_WATT: Final[UnitPowerT] = UnitPowerT(UnitT("kW")) +POWER_WATT: Final = "W" +POWER_KILO_WATT: Final = "kW" # Voltage units -VOLT: Final[UnitT] = UnitT("V") +VOLT: Final = "V" # Energy units -UnitEnergyT = NewType("UnitEnergyT", UnitT) -ENERGY_WATT_HOUR: Final[UnitEnergyT] = UnitEnergyT(UnitT("Wh")) -ENERGY_KILO_WATT_HOUR: Final[UnitEnergyT] = UnitEnergyT(UnitT("kWh")) +ENERGY_WATT_HOUR: Final = "Wh" +ENERGY_KILO_WATT_HOUR: Final = "kWh" # Electrical units -ELECTRICAL_CURRENT_AMPERE: Final[UnitT] = UnitT("A") -ELECTRICAL_VOLT_AMPERE: Final[UnitT] = UnitT("VA") +ELECTRICAL_CURRENT_AMPERE: Final = "A" +ELECTRICAL_VOLT_AMPERE: Final = "VA" # Degree units -DEGREE: Final[UnitT] = UnitT("°") +DEGREE: Final = "°" # Currency units -UnitCurrencyT = NewType("UnitCurrencyT", UnitT) -CURRENCY_EURO: Final[UnitCurrencyT] = UnitCurrencyT(UnitT("€")) -CURRENCY_DOLLAR: Final[UnitCurrencyT] = UnitCurrencyT(UnitT("$")) -CURRENCY_CENT: Final[UnitCurrencyT] = UnitCurrencyT(UnitT("¢")) +CURRENCY_EURO: Final = "€" +CURRENCY_DOLLAR: Final = "$" +CURRENCY_CENT: Final = "¢" # Temperature units -UnitTemperatureT = NewType("UnitTemperatureT", UnitT) -TEMP_CELSIUS: Final[UnitTemperatureT] = UnitTemperatureT(UnitT("°C")) -TEMP_FAHRENHEIT: Final[UnitTemperatureT] = UnitTemperatureT(UnitT("°F")) -TEMP_KELVIN: Final[UnitTemperatureT] = UnitTemperatureT(UnitT("K")) +TEMP_CELSIUS: Final = "°C" +TEMP_FAHRENHEIT: Final = "°F" +TEMP_KELVIN: Final = "K" # Time units -UnitTimeT = NewType("UnitTimeT", UnitT) -TIME_MICROSECONDS: Final[UnitTimeT] = UnitTimeT(UnitT("μs")) -TIME_MILLISECONDS: Final[UnitTimeT] = UnitTimeT(UnitT("ms")) -TIME_SECONDS: Final[UnitTimeT] = UnitTimeT(UnitT("s")) -TIME_MINUTES: Final[UnitTimeT] = UnitTimeT(UnitT("min")) -TIME_HOURS: Final[UnitTimeT] = UnitTimeT(UnitT("h")) -TIME_DAYS: Final[UnitTimeT] = UnitTimeT(UnitT("d")) -TIME_WEEKS: Final[UnitTimeT] = UnitTimeT(UnitT("w")) -TIME_MONTHS: Final[UnitTimeT] = UnitTimeT(UnitT("m")) -TIME_YEARS: Final[UnitTimeT] = UnitTimeT(UnitT("y")) +TIME_MICROSECONDS: Final = "μs" +TIME_MILLISECONDS: Final = "ms" +TIME_SECONDS: Final = "s" +TIME_MINUTES: Final = "min" +TIME_HOURS: Final = "h" +TIME_DAYS: Final = "d" +TIME_WEEKS: Final = "w" +TIME_MONTHS: Final = "m" +TIME_YEARS: Final = "y" # Length units -UnitLengthT = NewType("UnitLengthT", UnitT) -LENGTH_MILLIMETERS: Final[UnitLengthT] = UnitLengthT(UnitT("mm")) -LENGTH_CENTIMETERS: Final[UnitLengthT] = UnitLengthT(UnitT("cm")) -LENGTH_METERS: Final[UnitLengthT] = UnitLengthT(UnitT("m")) -LENGTH_KILOMETERS: Final[UnitLengthT] = UnitLengthT(UnitT("km")) +LENGTH_MILLIMETERS: Final = "mm" +LENGTH_CENTIMETERS: Final = "cm" +LENGTH_METERS: Final = "m" +LENGTH_KILOMETERS: Final = "km" -LENGTH_INCHES: Final[UnitLengthT] = UnitLengthT(UnitT("in")) -LENGTH_FEET: Final[UnitLengthT] = UnitLengthT(UnitT("ft")) -LENGTH_YARD: Final[UnitLengthT] = UnitLengthT(UnitT("yd")) -LENGTH_MILES: Final[UnitLengthT] = UnitLengthT(UnitT("mi")) +LENGTH_INCHES: Final = "in" +LENGTH_FEET: Final = "ft" +LENGTH_YARD: Final = "yd" +LENGTH_MILES: Final = "mi" # Frequency units -UnitFrequencyT = NewType("UnitFrequencyT", UnitT) -FREQUENCY_HERTZ: Final[UnitFrequencyT] = UnitFrequencyT(UnitT("Hz")) -FREQUENCY_GIGAHERTZ: Final[UnitFrequencyT] = UnitFrequencyT(UnitT("GHz")) +FREQUENCY_HERTZ: Final = "Hz" +FREQUENCY_GIGAHERTZ: Final = "GHz" # Pressure units -UnitPressureT = NewType("UnitPressureT", UnitT) -PRESSURE_PA: Final[UnitPressureT] = UnitPressureT(UnitT("Pa")) -PRESSURE_HPA: Final[UnitPressureT] = UnitPressureT(UnitT("hPa")) -PRESSURE_BAR: Final[UnitPressureT] = UnitPressureT(UnitT("bar")) -PRESSURE_MBAR: Final[UnitPressureT] = UnitPressureT(UnitT("mbar")) -PRESSURE_INHG: Final[UnitPressureT] = UnitPressureT(UnitT("inHg")) -PRESSURE_PSI: Final[UnitPressureT] = UnitPressureT(UnitT("psi")) +PRESSURE_PA: Final = "Pa" +PRESSURE_HPA: Final = "hPa" +PRESSURE_BAR: Final = "bar" +PRESSURE_MBAR: Final = "mbar" +PRESSURE_INHG: Final = "inHg" +PRESSURE_PSI: Final = "psi" # Sound pressure units -UnitSoundPressureT = NewType("UnitSoundPressureT", UnitT) -SOUND_PRESSURE_DB: Final[UnitSoundPressureT] = UnitSoundPressureT(UnitT("dB")) -SOUND_PRESSURE_WEIGHTED_DBA: Final[UnitSoundPressureT] = UnitSoundPressureT( - UnitT("dBa") -) +SOUND_PRESSURE_DB: Final = "dB" +SOUND_PRESSURE_WEIGHTED_DBA: Final = "dBa" # Volume units -UnitVolumeT = NewType("UnitVolumeT", UnitT) -VOLUME_LITERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("L")) -VOLUME_MILLILITERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("mL")) -VOLUME_CUBIC_METERS: Final[UnitVolumeT] = UnitVolumeT(UnitT("m³")) -VOLUME_CUBIC_FEET: Final[UnitVolumeT] = UnitVolumeT(UnitT("ft³")) +VOLUME_LITERS: Final = "L" +VOLUME_MILLILITERS: Final = "mL" +VOLUME_CUBIC_METERS: Final = "m³" +VOLUME_CUBIC_FEET: Final = "ft³" -VOLUME_GALLONS: Final[UnitVolumeT] = UnitVolumeT(UnitT("gal")) -VOLUME_FLUID_OUNCE: Final[UnitVolumeT] = UnitVolumeT(UnitT("fl. oz.")) +VOLUME_GALLONS: Final = "gal" +VOLUME_FLUID_OUNCE: Final = "fl. oz." # Volume Flow Rate units -UnitVolumeFlowT = NewType("UnitVolumeFlowT", UnitT) -VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final[UnitVolumeFlowT] = UnitVolumeFlowT( - UnitT("m³/h") -) -VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final[UnitVolumeFlowT] = UnitVolumeFlowT( - UnitT("ft³/m") -) +VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h" +VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = "ft³/m" # Area units -UnitAreaT = NewType("UnitAreaT", UnitT) -AREA_SQUARE_METERS: Final[UnitAreaT] = UnitAreaT(UnitT("m²")) +AREA_SQUARE_METERS: Final = "m²" # Mass units -UnitMassT = NewType("UnitMassT", UnitT) -MASS_GRAMS: Final[UnitMassT] = UnitMassT(UnitT("g")) -MASS_KILOGRAMS: Final[UnitMassT] = UnitMassT(UnitT("kg")) -MASS_MILLIGRAMS: Final[UnitMassT] = UnitMassT(UnitT("mg")) -MASS_MICROGRAMS: Final[UnitMassT] = UnitMassT(UnitT("µg")) +MASS_GRAMS: Final = "g" +MASS_KILOGRAMS: Final = "kg" +MASS_MILLIGRAMS: Final = "mg" +MASS_MICROGRAMS: Final = "µg" -MASS_OUNCES: Final[UnitMassT] = UnitMassT(UnitT("oz")) -MASS_POUNDS: Final[UnitMassT] = UnitMassT(UnitT("lb")) +MASS_OUNCES: Final = "oz" +MASS_POUNDS: Final = "lb" # Conductivity units -CONDUCTIVITY: Final[UnitT] = UnitT("µS/cm") +CONDUCTIVITY: Final = "µS/cm" # Light units -LIGHT_LUX: Final[UnitT] = UnitT("lx") +LIGHT_LUX: Final = "lx" # UV Index units -UV_INDEX: Final[UnitT] = UnitT("UV index") +UV_INDEX: Final = "UV index" # Percentage units -PERCENTAGE: Final[UnitT] = UnitT("%") +PERCENTAGE: Final = "%" # Irradiation units -UnitIrradiationT = NewType("UnitIrradiationT", UnitT) -IRRADIATION_WATTS_PER_SQUARE_METER: Final[UnitIrradiationT] = UnitIrradiationT( - UnitT("W/m²") -) -IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final[UnitIrradiationT] = UnitIrradiationT( - UnitT("BTU/(h×ft²)") -) +IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" +IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)" # Precipitation units -PRECIPITATION_MILLIMETERS_PER_HOUR: Final[UnitT] = UnitT("mm/h") +PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" # Concentration units -UnitConcentrationT = NewType("UnitConcentrationT", UnitT) -CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final[ - UnitConcentrationT -] = UnitConcentrationT(UnitT("µg/m³")) -CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final[ - UnitConcentrationT -] = UnitConcentrationT(UnitT("mg/m³")) -CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final[UnitConcentrationT] = UnitConcentrationT( - UnitT("μg/ft³") -) -CONCENTRATION_PARTS_PER_CUBIC_METER: Final[UnitConcentrationT] = UnitConcentrationT( - UnitT("p/m³") -) -CONCENTRATION_PARTS_PER_MILLION: Final[UnitConcentrationT] = UnitConcentrationT( - UnitT("ppm") -) -CONCENTRATION_PARTS_PER_BILLION: Final[UnitConcentrationT] = UnitConcentrationT( - UnitT("ppb") -) +CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" +CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" +CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" +CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" +CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" +CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" # Speed units -UnitSpeedT = NewType("UnitSpeedT", UnitT) -SPEED_MILLIMETERS_PER_DAY: Final[UnitSpeedT] = UnitSpeedT(UnitT("mm/d")) -SPEED_INCHES_PER_DAY: Final[UnitSpeedT] = UnitSpeedT(UnitT("in/d")) -SPEED_METERS_PER_SECOND: Final[UnitSpeedT] = UnitSpeedT(UnitT("m/s")) -SPEED_INCHES_PER_HOUR: Final[UnitSpeedT] = UnitSpeedT(UnitT("in/h")) -SPEED_KILOMETERS_PER_HOUR: Final[UnitSpeedT] = UnitSpeedT(UnitT("km/h")) -SPEED_MILES_PER_HOUR: Final[UnitSpeedT] = UnitSpeedT(UnitT("mph")) +SPEED_MILLIMETERS_PER_DAY: Final = "mm/d" +SPEED_INCHES_PER_DAY: Final = "in/d" +SPEED_METERS_PER_SECOND: Final = "m/s" +SPEED_INCHES_PER_HOUR: Final = "in/h" +SPEED_KILOMETERS_PER_HOUR: Final = "km/h" +SPEED_MILES_PER_HOUR: Final = "mph" # Signal_strength units -UnitSignalStrengthT = NewType("UnitSignalStrengthT", UnitT) -SIGNAL_STRENGTH_DECIBELS: Final[UnitSignalStrengthT] = UnitSignalStrengthT(UnitT("dB")) -SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final[UnitSignalStrengthT] = UnitSignalStrengthT( - UnitT("dBm") -) +SIGNAL_STRENGTH_DECIBELS: Final = "dB" +SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" # Data units -UnitDataT = NewType("UnitDataT", UnitT) -DATA_BITS: Final[UnitDataT] = UnitDataT(UnitT("bit")) -DATA_KILOBITS: Final[UnitDataT] = UnitDataT(UnitT("kbit")) -DATA_MEGABITS: Final[UnitDataT] = UnitDataT(UnitT("Mbit")) -DATA_GIGABITS: Final[UnitDataT] = UnitDataT(UnitT("Gbit")) -DATA_BYTES: Final[UnitDataT] = UnitDataT(UnitT("B")) -DATA_KILOBYTES: Final[UnitDataT] = UnitDataT(UnitT("kB")) -DATA_MEGABYTES: Final[UnitDataT] = UnitDataT(UnitT("MB")) -DATA_GIGABYTES: Final[UnitDataT] = UnitDataT(UnitT("GB")) -DATA_TERABYTES: Final[UnitDataT] = UnitDataT(UnitT("TB")) -DATA_PETABYTES: Final[UnitDataT] = UnitDataT(UnitT("PB")) -DATA_EXABYTES: Final[UnitDataT] = UnitDataT(UnitT("EB")) -DATA_ZETTABYTES: Final[UnitDataT] = UnitDataT(UnitT("ZB")) -DATA_YOTTABYTES: Final[UnitDataT] = UnitDataT(UnitT("YB")) -DATA_KIBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("KiB")) -DATA_MEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("MiB")) -DATA_GIBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("GiB")) -DATA_TEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("TiB")) -DATA_PEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("PiB")) -DATA_EXBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("EiB")) -DATA_ZEBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("ZiB")) -DATA_YOBIBYTES: Final[UnitDataT] = UnitDataT(UnitT("YiB")) +DATA_BITS: Final = "bit" +DATA_KILOBITS: Final = "kbit" +DATA_MEGABITS: Final = "Mbit" +DATA_GIGABITS: Final = "Gbit" +DATA_BYTES: Final = "B" +DATA_KILOBYTES: Final = "kB" +DATA_MEGABYTES: Final = "MB" +DATA_GIGABYTES: Final = "GB" +DATA_TERABYTES: Final = "TB" +DATA_PETABYTES: Final = "PB" +DATA_EXABYTES: Final = "EB" +DATA_ZETTABYTES: Final = "ZB" +DATA_YOTTABYTES: Final = "YB" +DATA_KIBIBYTES: Final = "KiB" +DATA_MEBIBYTES: Final = "MiB" +DATA_GIBIBYTES: Final = "GiB" +DATA_TEBIBYTES: Final = "TiB" +DATA_PEBIBYTES: Final = "PiB" +DATA_EXBIBYTES: Final = "EiB" +DATA_ZEBIBYTES: Final = "ZiB" +DATA_YOBIBYTES: Final = "YiB" # Data_rate units -UnitDataRateT = NewType("UnitDataRateT", UnitT) -DATA_RATE_BITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("bit/s")) -DATA_RATE_KILOBITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("kbit/s")) -DATA_RATE_MEGABITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("Mbit/s")) -DATA_RATE_GIGABITS_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("Gbit/s")) -DATA_RATE_BYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("B/s")) -DATA_RATE_KILOBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("kB/s")) -DATA_RATE_MEGABYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("MB/s")) -DATA_RATE_GIGABYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("GB/s")) -DATA_RATE_KIBIBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("KiB/s")) -DATA_RATE_MEBIBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("MiB/s")) -DATA_RATE_GIBIBYTES_PER_SECOND: Final[UnitDataRateT] = UnitDataRateT(UnitT("GiB/s")) +DATA_RATE_BITS_PER_SECOND: Final = "bit/s" +DATA_RATE_KILOBITS_PER_SECOND: Final = "kbit/s" +DATA_RATE_MEGABITS_PER_SECOND: Final = "Mbit/s" +DATA_RATE_GIGABITS_PER_SECOND: Final = "Gbit/s" +DATA_RATE_BYTES_PER_SECOND: Final = "B/s" +DATA_RATE_KILOBYTES_PER_SECOND: Final = "kB/s" +DATA_RATE_MEGABYTES_PER_SECOND: Final = "MB/s" +DATA_RATE_GIGABYTES_PER_SECOND: Final = "GB/s" +DATA_RATE_KIBIBYTES_PER_SECOND: Final = "KiB/s" +DATA_RATE_MEBIBYTES_PER_SECOND: Final = "MiB/s" +DATA_RATE_GIBIBYTES_PER_SECOND: Final = "GiB/s" # #### SERVICES #### @@ -706,14 +661,13 @@ RESTART_EXIT_CODE: Final = 100 UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit." -UnitTypeT = NewType("UnitTypeT", str) -LENGTH: Final[UnitTypeT] = UnitTypeT("length") -MASS: Final[UnitTypeT] = UnitTypeT("mass") -PRESSURE: Final[UnitTypeT] = UnitTypeT("pressure") -VOLUME: Final[UnitTypeT] = UnitTypeT("volume") -TEMPERATURE: Final[UnitTypeT] = UnitTypeT("temperature") -SPEED_MS: Final[UnitTypeT] = UnitTypeT("speed_ms") -ILLUMINANCE: Final[UnitTypeT] = UnitTypeT("illuminance") +LENGTH: Final = "length" +MASS: Final = "mass" +PRESSURE: Final = "pressure" +VOLUME: Final = "volume" +TEMPERATURE: Final = "temperature" +SPEED_MS: Final = "speed_ms" +ILLUMINANCE: Final = "illuminance" WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 2693fc13fc2..e0f089e93b9 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -3,16 +3,13 @@ from __future__ import annotations from numbers import Number -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitTemperatureT +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant from homeassistant.util.temperature import convert as convert_temperature def display_temp( - hass: HomeAssistant, - temperature: float | None, - unit: UnitTemperatureT, - precision: float, + hass: HomeAssistant, temperature: float | None, unit: str, precision: float ) -> float | None: """Convert temperature into preferred units/precision for display.""" temperature_unit = unit diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 921e2941760..6b21e9b4c47 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -15,10 +15,9 @@ from homeassistant.const import ( LENGTH_MILLIMETERS, LENGTH_YARD, UNIT_NOT_RECOGNIZED_TEMPLATE, - UnitLengthT, ) -VALID_UNITS: tuple[UnitLengthT, ...] = ( +VALID_UNITS: tuple[str, ...] = ( LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_FEET, @@ -29,7 +28,7 @@ VALID_UNITS: tuple[UnitLengthT, ...] = ( LENGTH_YARD, ) -TO_METERS: dict[UnitLengthT, Callable[[float], float]] = { +TO_METERS: dict[str, Callable[[float], float]] = { LENGTH_METERS: lambda meters: meters, LENGTH_MILES: lambda miles: miles * 1609.344, LENGTH_YARD: lambda yards: yards * 0.9144, @@ -40,7 +39,7 @@ TO_METERS: dict[UnitLengthT, Callable[[float], float]] = { LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001, } -METERS_TO: dict[UnitLengthT, Callable[[float], float]] = { +METERS_TO: dict[str, Callable[[float], float]] = { LENGTH_METERS: lambda meters: meters, LENGTH_MILES: lambda meters: meters * 0.000621371, LENGTH_YARD: lambda meters: meters * 1.09361, @@ -52,7 +51,7 @@ METERS_TO: dict[UnitLengthT, Callable[[float], float]] = { } -def convert(value: float, unit_1: UnitLengthT, unit_2: UnitLengthT) -> float: +def convert(value: float, unit_1: str, unit_2: str) -> float: """Convert one unit of measurement to another.""" if unit_1 not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, LENGTH)) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 9939b02e141..188cf66491e 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -11,10 +11,9 @@ from homeassistant.const import ( PRESSURE_PA, PRESSURE_PSI, UNIT_NOT_RECOGNIZED_TEMPLATE, - UnitPressureT, ) -VALID_UNITS: tuple[UnitPressureT, ...] = ( +VALID_UNITS: tuple[str, ...] = ( PRESSURE_PA, PRESSURE_HPA, PRESSURE_MBAR, @@ -22,7 +21,7 @@ VALID_UNITS: tuple[UnitPressureT, ...] = ( PRESSURE_PSI, ) -UNIT_CONVERSION: dict[UnitPressureT, float] = { +UNIT_CONVERSION: dict[str, float] = { PRESSURE_PA: 1, PRESSURE_HPA: 1 / 100, PRESSURE_MBAR: 1 / 100, @@ -31,7 +30,7 @@ UNIT_CONVERSION: dict[UnitPressureT, float] = { } -def convert(value: float, unit_1: UnitPressureT, unit_2: UnitPressureT) -> float: +def convert(value: float, unit_1: str, unit_2: str) -> float: """Convert one unit of measurement to another.""" if unit_1 not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, PRESSURE)) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 015f33383d1..bc3cb4c1017 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -5,7 +5,6 @@ from homeassistant.const import ( TEMP_KELVIN, TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE, - UnitTemperatureT, ) @@ -38,10 +37,7 @@ def celsius_to_kelvin(celsius: float, interval: bool = False) -> float: def convert( - temperature: float, - from_unit: UnitTemperatureT, - to_unit: UnitTemperatureT, - interval: bool = False, + temperature: float, from_unit: str, to_unit: str, interval: bool = False ) -> float: """Convert a temperature from one unit to another.""" if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN): diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 59dbf784152..bdd47112dde 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -24,13 +24,6 @@ from homeassistant.const import ( VOLUME, VOLUME_GALLONS, VOLUME_LITERS, - UnitLengthT, - UnitMassT, - UnitPressureT, - UnitT, - UnitTemperatureT, - UnitTypeT, - UnitVolumeT, ) from homeassistant.util import ( distance as distance_util, @@ -43,23 +36,17 @@ from homeassistant.util import ( LENGTH_UNITS = distance_util.VALID_UNITS -MASS_UNITS: tuple[UnitMassT, ...] = ( - MASS_POUNDS, - MASS_OUNCES, - MASS_KILOGRAMS, - MASS_GRAMS, -) +MASS_UNITS: tuple[str, ...] = (MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS) PRESSURE_UNITS = pressure_util.VALID_UNITS VOLUME_UNITS = volume_util.VALID_UNITS -TEMPERATURE_UNITS: tuple[UnitTemperatureT, ...] = (TEMP_FAHRENHEIT, TEMP_CELSIUS) +TEMPERATURE_UNITS: tuple[str, ...] = (TEMP_FAHRENHEIT, TEMP_CELSIUS) -def is_valid_unit(unit: UnitT, unit_type: UnitTypeT) -> bool: +def is_valid_unit(unit: str, unit_type: str) -> bool: """Check if the unit is valid for it's type.""" - units: tuple[UnitT, ...] if unit_type == LENGTH: units = LENGTH_UNITS elif unit_type == TEMPERATURE: @@ -82,11 +69,11 @@ class UnitSystem: def __init__( self, name: str, - temperature: UnitTemperatureT, - length: UnitLengthT, - volume: UnitVolumeT, - mass: UnitMassT, - pressure: UnitPressureT, + temperature: str, + length: str, + volume: str, + mass: str, + pressure: str, ) -> None: """Initialize the unit system object.""" errors: str = ", ".join( @@ -116,14 +103,14 @@ class UnitSystem: """Determine if this is the metric unit system.""" return self.name == CONF_UNIT_SYSTEM_METRIC - def temperature(self, temperature: float, from_unit: UnitTemperatureT) -> float: + def temperature(self, temperature: float, from_unit: str) -> float: """Convert the given temperature to this unit system.""" if not isinstance(temperature, Number): raise TypeError(f"{temperature!s} is not a numeric value.") return temperature_util.convert(temperature, from_unit, self.temperature_unit) - def length(self, length: float | None, from_unit: UnitLengthT) -> float: + def length(self, length: float | None, from_unit: str) -> float: """Convert the given length to this unit system.""" if not isinstance(length, Number): raise TypeError(f"{length!s} is not a numeric value.") @@ -133,7 +120,7 @@ class UnitSystem: length, from_unit, self.length_unit ) - def pressure(self, pressure: float | None, from_unit: UnitPressureT) -> float: + def pressure(self, pressure: float | None, from_unit: str) -> float: """Convert the given pressure to this unit system.""" if not isinstance(pressure, Number): raise TypeError(f"{pressure!s} is not a numeric value.") @@ -143,7 +130,7 @@ class UnitSystem: pressure, from_unit, self.pressure_unit ) - def volume(self, volume: float | None, from_unit: UnitVolumeT) -> float: + def volume(self, volume: float | None, from_unit: str) -> float: """Convert the given volume to this unit system.""" if not isinstance(volume, Number): raise TypeError(f"{volume!s} is not a numeric value.") @@ -151,7 +138,7 @@ class UnitSystem: # type ignore: https://github.com/python/mypy/issues/7207 return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore - def as_dict(self) -> dict[str, UnitT]: + def as_dict(self) -> dict[str, str]: """Convert the unit system to a dictionary.""" return { LENGTH: self.length_unit, diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index ddf2be5a3bc..f4a02dbe82e 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -10,10 +10,9 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, - UnitVolumeT, ) -VALID_UNITS: tuple[UnitVolumeT, ...] = ( +VALID_UNITS: tuple[str, ...] = ( VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, @@ -31,7 +30,7 @@ def __gallon_to_liter(gallon: float) -> float: return gallon * 3.785 -def convert(volume: float, from_unit: UnitVolumeT, to_unit: UnitVolumeT) -> float: +def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a temperature from one unit to another.""" if from_unit not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, VOLUME)) From 08f03c95d28bea069c098d4d1f09657b684a372d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 20 Jul 2021 08:18:09 -0400 Subject: [PATCH 409/818] Use entity class attributes for Brunt (#53164) --- homeassistant/components/brunt/cover.py | 77 ++++++------------------- 1 file changed, 18 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 9c539fe51fe..5c9d7b3d4a5 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -1,4 +1,5 @@ """Support for Brunt Blind Engine covers.""" +from __future__ import annotations import logging @@ -69,37 +70,19 @@ class BruntDevice(CoverEntity): Contains the common logic for all Brunt devices. """ + _attr_device_class = DEVICE_CLASS_WINDOW + _attr_supported_features = COVER_FEATURES + def __init__(self, bapi, name, thing_uri): """Init the Brunt device.""" self._bapi = bapi - self._name = name + self._attr_name = name self._thing_uri = thing_uri self._state = {} - self._available = None @property - def name(self): - """Return the name of the device as reported by tellcore.""" - return self._name - - @property - def available(self): - """Could the device be accessed during the last update call.""" - return self._available - - @property - def current_cover_position(self): - """ - Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - pos = self._state.get("currentPosition") - return int(pos) if pos else None - - @property - def request_cover_position(self): + def request_cover_position(self) -> int | None: """ Return request position of cover. @@ -111,7 +94,7 @@ class BruntDevice(CoverEntity): return int(pos) if pos else None @property - def move_state(self): + def move_state(self) -> int | None: """ Return current moving state of cover. @@ -120,47 +103,23 @@ class BruntDevice(CoverEntity): mov = self._state.get("moveState") return int(mov) if mov else None - @property - def is_opening(self): - """Return if the cover is opening or not.""" - return self.move_state == 1 - - @property - def is_closing(self): - """Return if the cover is closing or not.""" - return self.move_state == 2 - - @property - def extra_state_attributes(self): - """Return the detailed device state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_REQUEST_POSITION: self.request_cover_position, - } - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_WINDOW - - @property - def supported_features(self): - """Flag supported features.""" - return COVER_FEATURES - - @property - def is_closed(self): - """Return true if cover is closed, else False.""" - return self.current_cover_position == CLOSED_POSITION - def update(self): """Poll the current state of the device.""" try: self._state = self._bapi.getState(thingUri=self._thing_uri).get("thing") - self._available = True + self._attr_available = True except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) - self._available = False + self._attr_available = False + self._attr_is_opening = self.move_state == 1 + self._attr_is_closing = self.move_state == 2 + pos = self._state.get("currentPosition") + self._attr_current_cover_position = int(pos) if pos else None + self._attr_is_closed = self.current_cover_position == CLOSED_POSITION + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_REQUEST_POSITION: self.request_cover_position, + } def open_cover(self, **kwargs): """Set the cover to the open position.""" From c578541a82f5ca5420c3d3fb69eace4d9d57b28d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Jul 2021 15:57:11 +0200 Subject: [PATCH 410/818] Add new electrical unit constants (mV + mA) (#53158) --- homeassistant/components/bloomsky/sensor.py | 5 +++-- homeassistant/components/homematic/sensor.py | 3 ++- homeassistant/components/isy994/const.py | 6 ++++-- homeassistant/components/mysensors/sensor.py | 3 ++- homeassistant/components/omnilogic/sensor.py | 3 ++- homeassistant/components/ondilo_ico/sensor.py | 8 +++++++- homeassistant/components/poolsense/sensor.py | 7 ++++--- homeassistant/components/wled/const.py | 3 --- homeassistant/components/wled/sensor.py | 5 +++-- homeassistant/const.py | 2 ++ tests/components/wled/test_sensor.py | 12 +++++------- 11 files changed, 34 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 29ca198c1fc..7aa2fe9baba 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -6,6 +6,7 @@ from homeassistant.const import ( AREA_SQUARE_METERS, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, PRESSURE_INHG, PRESSURE_MBAR, @@ -32,7 +33,7 @@ SENSOR_UNITS_IMPERIAL = { "Humidity": PERCENTAGE, "Pressure": PRESSURE_INHG, "Luminance": f"cd/{AREA_SQUARE_METERS}", - "Voltage": "mV", + "Voltage": ELECTRIC_POTENTIAL_MILLIVOLT, } # Metric units @@ -41,7 +42,7 @@ SENSOR_UNITS_METRIC = { "Humidity": PERCENTAGE, "Pressure": PRESSURE_MBAR, "Luminance": f"cd/{AREA_SQUARE_METERS}", - "Voltage": "mV", + "Voltage": ELECTRIC_POTENTIAL_MILLIVOLT, } # Device class diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 62f2f0ccdff..2bc51f67896 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_CURRENT_MILLIAMPERE, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_MILLIMETERS, @@ -47,7 +48,7 @@ HM_UNIT_HA_CAST = { "ACTUAL_TEMPERATURE": TEMP_CELSIUS, "BRIGHTNESS": "#", "POWER": POWER_WATT, - "CURRENT": "mA", + "CURRENT": ELECTRIC_CURRENT_MILLIAMPERE, "VOLTAGE": VOLT, "ENERGY_COUNTER": ENERGY_WATT_HOUR, "GAS_POWER": VOLUME_CUBIC_METERS, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 34b89baad68..03b1fa6c66b 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -48,6 +48,8 @@ from homeassistant.const import ( CURRENCY_CENT, CURRENCY_DOLLAR, DEGREE, + ELECTRIC_CURRENT_MILLIAMPERE, + ELECTRIC_POTENTIAL_MILLIVOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, @@ -371,9 +373,9 @@ UOM_FRIENDLY_NAME = { "38": LENGTH_METERS, "39": VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, "40": SPEED_METERS_PER_SECOND, - "41": "mA", + "41": ELECTRIC_CURRENT_MILLIAMPERE, "42": TIME_MILLISECONDS, - "43": "mV", + "43": ELECTRIC_POTENTIAL_MILLIVOLT, "44": TIME_MINUTES, "45": TIME_MINUTES, "46": PRECIPITATION_MILLIMETERS_PER_HOUR, diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 467ab761124..f16f9e09c41 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_POTENTIAL_MILLIVOLT, ELECTRICAL_CURRENT_AMPERE, ELECTRICAL_VOLT_AMPERE, ENERGY_KILO_WATT_HOUR, @@ -61,7 +62,7 @@ SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { "V_VOLTAGE": [VOLT, "mdi:flash", None], "V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto", None], "V_PH": ["pH", None, None], - "V_ORP": ["mV", None, None], + "V_ORP": [ELECTRIC_POTENTIAL_MILLIVOLT, None, None], "V_EC": [CONDUCTIVITY, None, None], "V_VAR": ["var", None, None], "V_VA": [ELECTRICAL_VOLT_AMPERE, None, None], diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index beec071b192..1f8de082868 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, + ELECTRIC_POTENTIAL_MILLIVOLT, MASS_GRAMS, PERCENTAGE, TEMP_CELSIUS, @@ -342,7 +343,7 @@ SENSOR_TYPES = { "kind": "csad_orp", "device_class": None, "icon": "mdi:gauge", - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "guard_condition": [ {"orp": ""}, ], diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 2428862cb31..633e03157e4 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, TEMP_CELSIUS, ) @@ -28,7 +29,12 @@ SENSOR_TYPES = { None, DEVICE_CLASS_TEMPERATURE, ], - "orp": ["Oxydo Reduction Potential", "mV", "mdi:pool", None], + "orp": [ + "Oxydo Reduction Potential", + ELECTRIC_POTENTIAL_MILLIVOLT, + "mdi:pool", + None, + ], "ph": ["pH", "", "mdi:pool", None], "tds": ["TDS", CONCENTRATION_PARTS_PER_MILLION, "mdi:pool", None], "battery": ["Battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY], diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index ca79fde6b08..dd03111e85e 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -6,6 +6,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, TEMP_CELSIUS, ) @@ -15,7 +16,7 @@ from .const import ATTRIBUTION, DOMAIN SENSORS = { "Chlorine": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Chlorine", "device_class": None, @@ -40,13 +41,13 @@ SENSORS = { "device_class": DEVICE_CLASS_TIMESTAMP, }, "Chlorine High": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Chlorine High", "device_class": None, }, "Chlorine Low": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Chlorine Low", "device_class": None, diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index d80dbf16a60..765de468350 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -30,9 +30,6 @@ ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" ATTR_UDP_PORT = "udp_port" -# Units of measurement -CURRENT_MA = "mA" - # Services SERVICE_EFFECT = "effect" SERVICE_PRESET = "preset" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 37311e333c3..634f903c020 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( DATA_BYTES, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TIMESTAMP, + ELECTRIC_CURRENT_MILLIAMPERE, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) @@ -17,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN +from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, DOMAIN from .coordinator import WLEDDataUpdateCoordinator from .models import WLEDEntity @@ -47,7 +48,7 @@ class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): """Defines a WLED estimated current sensor.""" _attr_icon = "mdi:power" - _attr_unit_of_measurement = CURRENT_MA + _attr_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE _attr_device_class = DEVICE_CLASS_CURRENT def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: diff --git a/homeassistant/const.py b/homeassistant/const.py index 6e2b7e17ce0..fb47ef7cb7c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -407,8 +407,10 @@ ENERGY_WATT_HOUR: Final = "Wh" ENERGY_KILO_WATT_HOUR: Final = "kWh" # Electrical units +ELECTRIC_CURRENT_MILLIAMPERE: Final = "mA" ELECTRICAL_CURRENT_AMPERE: Final = "A" ELECTRICAL_VOLT_AMPERE: Final = "VA" +ELECTRIC_POTENTIAL_MILLIVOLT: Final = "mV" # Degree units DEGREE: Final = "°" diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 4f2b07f4f51..e6e4b130d99 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -10,17 +10,13 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TIMESTAMP, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.components.wled.const import ( - ATTR_LED_COUNT, - ATTR_MAX_POWER, - CURRENT_MA, - DOMAIN, -) +from homeassistant.components.wled.const import ATTR_LED_COUNT, ATTR_MAX_POWER, DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES, + ELECTRIC_CURRENT_MILLIAMPERE, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNKNOWN, @@ -101,7 +97,9 @@ async def test_sensors( assert state.attributes.get(ATTR_ICON) == "mdi:power" assert state.attributes.get(ATTR_LED_COUNT) == 30 assert state.attributes.get(ATTR_MAX_POWER) == 850 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENT_MA + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_MILLIAMPERE + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT assert state.state == "470" From f819be7acc9e80a46439238a48aa26b14699ecb6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 17:26:00 +0200 Subject: [PATCH 411/818] Correct typing in Insteon and activate mypy (#53222) --- homeassistant/components/insteon/fan.py | 4 +++- homeassistant/components/insteon/ipdb.py | 2 +- homeassistant/components/insteon/schemas.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 00ada3e9a58..b76661d7dde 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,4 +1,6 @@ """Support for INSTEON fans via PowerLinc Modem.""" +from __future__ import annotations + import math from homeassistant.components.fan import ( @@ -39,7 +41,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity): """An INSTEON fan entity.""" @property - def percentage(self) -> int: + def percentage(self) -> int | None: """Return the current speed percentage.""" if self._insteon_device_group.value is None: return None diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 48223981103..9b32bc40043 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -110,4 +110,4 @@ def get_device_platforms(device): def get_platform_groups(device, domain) -> dict: """Return the platforms that a device belongs in.""" - return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) + return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index c43df24b4cb..5fb46735f29 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -161,7 +161,7 @@ TRIGGER_SCENE_SCHEMA = vol.Schema( ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) -def normalize_byte_entry_to_int(entry: [int, bytes, str]): +def normalize_byte_entry_to_int(entry: int | bytes | str): """Format a hex entry value.""" if isinstance(entry, int): if entry in range(0, 256): diff --git a/mypy.ini b/mypy.ini index 9f37f46d713..4bfb8e22fe3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1277,9 +1277,6 @@ ignore_errors = true [mypy-homeassistant.components.input_number.*] ignore_errors = true -[mypy-homeassistant.components.insteon.*] -ignore_errors = true - [mypy-homeassistant.components.ipp.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 666575a86ed..720c9157e14 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -84,7 +84,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.influxdb.*", "homeassistant.components.input_datetime.*", "homeassistant.components.input_number.*", - "homeassistant.components.insteon.*", "homeassistant.components.ipp.*", "homeassistant.components.isy994.*", "homeassistant.components.izone.*", From 1fe2d0f9c80da77000aa2ac2ea4010352cb52454 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 20 Jul 2021 08:41:48 -0700 Subject: [PATCH 412/818] Address style issues in nest typing (#53236) * Add additional types for config flow Fixing style errors introduced by partial typing in pr #53214 * Address typing style errors Make all functions fully typed, follow up to pr #53214 --- homeassistant/components/nest/config_flow.py | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 2070f232d77..eeeae4b1ddd 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -17,11 +17,12 @@ import asyncio from collections import OrderedDict import logging import os +from typing import Any import async_timeout import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow @@ -80,7 +81,7 @@ class NestFlowHandler( self._reauth = False @classmethod - def register_sdm_api(cls, hass) -> None: + def register_sdm_api(cls, hass: HomeAssistant) -> None: """Configure the flow handler to use the SDM API.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} @@ -105,7 +106,7 @@ class NestFlowHandler( "prompt": "consent", } - async def async_oauth_create_entry(self, data: dict) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the SDM flow.""" assert self.is_sdm_api(), "Step only supported for SDM API" data[DATA_SDM] = {} @@ -129,13 +130,17 @@ class NestFlowHandler( return await super().async_oauth_create_entry(data) - async def async_step_reauth(self, user_input=None) -> FlowResult: + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Perform reauth upon an API authentication error.""" assert self.is_sdm_api(), "Step only supported for SDM API" self._reauth = True # Forces update of existing config entry return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None) -> FlowResult: + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm reauth dialog.""" assert self.is_sdm_api(), "Step only supported for SDM API" if user_input is None: @@ -145,7 +150,9 @@ class NestFlowHandler( ) return await self.async_step_user() - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self.is_sdm_api(): # Reauth will update an existing entry @@ -154,7 +161,9 @@ class NestFlowHandler( return await super().async_step_user(user_input) return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" assert not self.is_sdm_api(), "Step only supported for legacy API" @@ -179,7 +188,9 @@ class NestFlowHandler( data_schema=vol.Schema({vol.Required("flow_impl"): vol.In(list(flows))}), ) - async def async_step_link(self, user_input=None) -> FlowResult: + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Attempt to link with the Nest account. Route the user to a website to authenticate with Nest. Depending on @@ -226,7 +237,7 @@ class NestFlowHandler( errors=errors, ) - async def async_step_import(self, info) -> FlowResult: + async def async_step_import(self, info: dict[str, Any]) -> FlowResult: """Import existing auth from Nest.""" assert not self.is_sdm_api(), "Step only supported for legacy API" @@ -247,7 +258,9 @@ class NestFlowHandler( ) @callback - def _entry_from_tokens(self, title, flow, tokens) -> FlowResult: + def _entry_from_tokens( + self, title: str, flow: dict[str, Any], tokens: list[Any] | dict[Any, Any] + ) -> FlowResult: """Create an entry from tokens.""" return self.async_create_entry( title=title, data={"tokens": tokens, "impl_domain": flow["domain"]} From 2b9b346a2822dfa42bb14678e379479a15190e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 17:52:22 +0200 Subject: [PATCH 413/818] Address late review of Co2 signal (#53232) --- homeassistant/components/co2signal/config_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 044dd95cc3b..953a09719ec 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -114,9 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: new_entry_type = TYPE_USE_HOME - for entry in self._async_current_entries(include_ignore=True): - if entry.source == config_entries.SOURCE_IGNORE: - continue + for entry in self._async_current_entries(include_ignore=False): if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type: continue From 0cc4231ac20924c3efa46f7be739688679d7237a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 17:57:35 +0200 Subject: [PATCH 414/818] Tibber use dataclass (#53233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, use dataclass Signed-off-by: Daniel Hjelseth Høyer * Tibber, use dataclass Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 174 ++++++++++++---------- 1 file changed, 94 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 8a296fd93b2..65f2f5e17a1 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -1,6 +1,10 @@ """Support for Tibber sensors.""" +from __future__ import annotations + import asyncio +from dataclasses import dataclass from datetime import timedelta +from enum import Enum import logging from random import randrange @@ -45,108 +49,142 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) PARALLEL_UPDATES = 0 SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" -RT_SENSOR_MAP = { - "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT, None], - "power": ["power", DEVICE_CLASS_POWER, POWER_WATT, None], - "powerProduction": ["power production", DEVICE_CLASS_POWER, POWER_WATT, None], - "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT, None], - "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT, None], - "accumulatedConsumption": [ + +class ResetType(Enum): + """Data reset type.""" + + HOURLY = "hourly" + DAILY = "daily" + NEVER = "never" + + +@dataclass +class TibberSensorMetadata: + """Metadata for an individual Tibber sensor.""" + + name: str + device_class: str + unit: str | None = None + state_class: str | None = None + reset_type: ResetType | None = None + + +RT_SENSOR_MAP: dict[str, TibberSensorMetadata] = { + "averagePower": TibberSensorMetadata( + "average power", DEVICE_CLASS_POWER, POWER_WATT + ), + "power": TibberSensorMetadata( + "power", + DEVICE_CLASS_POWER, + POWER_WATT, + ), + "powerProduction": TibberSensorMetadata( + "power production", DEVICE_CLASS_POWER, POWER_WATT + ), + "minPower": TibberSensorMetadata("min power", DEVICE_CLASS_POWER, POWER_WATT), + "maxPower": TibberSensorMetadata("max power", DEVICE_CLASS_POWER, POWER_WATT), + "accumulatedConsumption": TibberSensorMetadata( "accumulated consumption", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, STATE_CLASS_MEASUREMENT, - ], - "accumulatedConsumptionLastHour": [ + ResetType.DAILY, + ), + "accumulatedConsumptionLastHour": TibberSensorMetadata( "accumulated consumption current hour", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, STATE_CLASS_MEASUREMENT, - ], - "accumulatedProduction": [ + ResetType.HOURLY, + ), + "accumulatedProduction": TibberSensorMetadata( "accumulated production", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, STATE_CLASS_MEASUREMENT, - ], - "accumulatedProductionLastHour": [ + ResetType.DAILY, + ), + "accumulatedProductionLastHour": TibberSensorMetadata( "accumulated production current hour", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, STATE_CLASS_MEASUREMENT, - ], - "lastMeterConsumption": [ + ResetType.HOURLY, + ), + "lastMeterConsumption": TibberSensorMetadata( "last meter consumption", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, STATE_CLASS_MEASUREMENT, - ], - "lastMeterProduction": [ + ), + "lastMeterProduction": TibberSensorMetadata( "last meter production", DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, STATE_CLASS_MEASUREMENT, - ], - "voltagePhase1": [ + ), + "voltagePhase1": TibberSensorMetadata( "voltage phase1", DEVICE_CLASS_VOLTAGE, VOLT, STATE_CLASS_MEASUREMENT, - ], - "voltagePhase2": [ + ), + "voltagePhase2": TibberSensorMetadata( "voltage phase2", DEVICE_CLASS_VOLTAGE, VOLT, STATE_CLASS_MEASUREMENT, - ], - "voltagePhase3": [ + ), + "voltagePhase3": TibberSensorMetadata( "voltage phase3", DEVICE_CLASS_VOLTAGE, VOLT, STATE_CLASS_MEASUREMENT, - ], - "currentL1": [ + ), + "currentL1": TibberSensorMetadata( "current L1", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT, - ], - "currentL2": [ + ), + "currentL2": TibberSensorMetadata( "current L2", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT, - ], - "currentL3": [ + ), + "currentL3": TibberSensorMetadata( "current L3", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT, - ], - "signalStrength": [ + ), + "signalStrength": TibberSensorMetadata( "signal strength", DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS, STATE_CLASS_MEASUREMENT, - ], - "accumulatedReward": [ + ), + "accumulatedReward": TibberSensorMetadata( "accumulated reward", DEVICE_CLASS_MONETARY, None, STATE_CLASS_MEASUREMENT, - ], - "accumulatedCost": [ + ResetType.DAILY, + ), + "accumulatedCost": TibberSensorMetadata( "accumulated cost", DEVICE_CLASS_MONETARY, None, STATE_CLASS_MEASUREMENT, - ], - "powerFactor": [ + ResetType.DAILY, + ), + "powerFactor": TibberSensorMetadata( "power factor", DEVICE_CLASS_POWER_FACTOR, PERCENTAGE, STATE_CLASS_MEASUREMENT, - ], + ), } @@ -312,39 +350,30 @@ class TibberSensorRT(TibberSensor): _attr_should_poll = False - def __init__( - self, tibber_home, sensor_name, device_class, unit, initial_state, state_class - ): + def __init__(self, tibber_home, metadata: TibberSensorMetadata, initial_state): """Initialize the sensor.""" super().__init__(tibber_home) - self._sensor_name = sensor_name self._model = "Tibber Pulse" self._device_name = f"{self._model} {self._home_name}" + self._metadata = metadata - self._attr_device_class = device_class - self._attr_name = f"{self._sensor_name} {self._home_name}" + self._attr_device_class = metadata.device_class + self._attr_name = f"{metadata.name} {self._home_name}" self._attr_state = initial_state - self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{self._sensor_name}" - self._attr_unit_of_measurement = unit - self._attr_state_class = state_class - if sensor_name in [ - "last meter consumption", - "last meter production", - ]: + self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{metadata.name}" + + if metadata.name in ["accumulated cost", "accumulated reward"]: + self._attr_unit_of_measurement = tibber_home.currency + else: + self._attr_unit_of_measurement = metadata.unit + self._attr_state_class = metadata.state_class + if metadata.reset_type == ResetType.NEVER: self._attr_last_reset = dt_util.utc_from_timestamp(0) - elif self._sensor_name in [ - "accumulated consumption", - "accumulated production", - "accumulated cost", - "accumulated reward", - ]: + elif metadata.reset_type == ResetType.DAILY: self._attr_last_reset = dt_util.as_utc( dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) ) - elif self._sensor_name in [ - "accumulated consumption current hour", - "accumulated production current hour", - ]: + elif metadata.reset_type == ResetType.HOURLY: self._attr_last_reset = dt_util.as_utc( dt_util.now().replace(minute=0, second=0, microsecond=0) ) @@ -369,19 +398,11 @@ class TibberSensorRT(TibberSensor): @callback def _set_state(self, state, timestamp): """Set sensor state.""" - if state < self._attr_state and self._sensor_name in [ - "accumulated consumption", - "accumulated production", - "accumulated cost", - "accumulated reward", - ]: + if state < self._attr_state and self._metadata.reset_type == ResetType.DAILY: self._attr_last_reset = dt_util.as_utc( timestamp.replace(hour=0, minute=0, second=0, microsecond=0) ) - if state < self._attr_state and self._sensor_name in [ - "accumulated consumption current hour", - "accumulated production current hour", - ]: + if state < self._attr_state and self._metadata.reset_type == ResetType.HOURLY: self._attr_last_reset = dt_util.as_utc( timestamp.replace(minute=0, second=0, microsecond=0) ) @@ -427,18 +448,11 @@ class TibberRtDataHandler: timestamp, ) else: - sensor_name, device_class, unit, state_class = RT_SENSOR_MAP[ - sensor_type - ] - if sensor_type in ["accumulatedCost", "accumulatedReward"]: - unit = self._tibber_home.currency + sensor_meta = RT_SENSOR_MAP[sensor_type] entity = TibberSensorRT( self._tibber_home, - sensor_name, - device_class, - unit, + sensor_meta, state, - state_class, ) new_entities.append(entity) self._entities[sensor_type] = entity.unique_id From 9b705ad6dfddaa97ea135b93ce570a284c5ae731 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 06:12:56 -1000 Subject: [PATCH 415/818] Update lock entity to support locking, unlocking, jammed (#51455) --- homeassistant/components/demo/lock.py | 76 +++++++-- homeassistant/components/lock/__init__.py | 27 ++++ .../components/lock/device_condition.py | 19 ++- .../components/lock/device_trigger.py | 13 +- .../components/lock/reproduce_state.py | 8 +- homeassistant/const.py | 3 + tests/components/demo/test_lock.py | 47 +++++- tests/components/google_assistant/__init__.py | 7 + .../components/lock/test_device_condition.py | 101 +++++++++++- tests/components/lock/test_device_trigger.py | 144 +++++++++++++++++- 10 files changed, 418 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index cafc0e3f748..7eabf9bea2d 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,6 +1,14 @@ """Demo lock platform that has two fake locks.""" +import asyncio + from homeassistant.components.lock import SUPPORT_OPEN, LockEntity -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -9,6 +17,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= [ DemoLock("Front Door", STATE_LOCKED), DemoLock("Kitchen Door", STATE_UNLOCKED), + DemoLock("Poorly Installed Door", STATE_UNLOCKED, False, True), DemoLock("Openable Lock", STATE_LOCKED, True), ] ) @@ -24,24 +33,67 @@ class DemoLock(LockEntity): _attr_should_poll = False - def __init__(self, name: str, state: str, openable: bool = False) -> None: + def __init__( + self, + name: str, + state: str, + openable: bool = False, + jam_on_operation: bool = False, + ) -> None: """Initialize the lock.""" self._attr_name = name - self._attr_is_locked = state == STATE_LOCKED if openable: self._attr_supported_features = SUPPORT_OPEN + self._state = state + self._openable = openable + self._jam_on_operation = jam_on_operation - def lock(self, **kwargs): + @property + def is_locking(self): + """Return true if lock is locking.""" + return self._state == STATE_LOCKING + + @property + def is_unlocking(self): + """Return true if lock is unlocking.""" + return self._state == STATE_UNLOCKING + + @property + def is_jammed(self): + """Return true if lock is jammed.""" + return self._state == STATE_JAMMED + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + async def async_lock(self, **kwargs): """Lock the device.""" - self._attr_is_locked = True - self.schedule_update_ha_state() + self._state = STATE_LOCKING + self.async_write_ha_state() + await asyncio.sleep(2) + if self._jam_on_operation: + self._state = STATE_JAMMED + else: + self._state = STATE_LOCKED + self.async_write_ha_state() - def unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Unlock the device.""" - self._attr_is_locked = False - self.schedule_update_ha_state() + self._state = STATE_UNLOCKING + self.async_write_ha_state() + await asyncio.sleep(2) + self._state = STATE_UNLOCKED + self.async_write_ha_state() - def open(self, **kwargs): + async def async_open(self, **kwargs): """Open the door latch.""" - self._attr_is_locked = False - self.schedule_update_ha_state() + self._state = STATE_UNLOCKED + self.async_write_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + if self._openable: + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 9e8bf3a740c..e98202a1ee5 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -15,8 +15,11 @@ from homeassistant.const import ( SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, + STATE_JAMMED, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -87,6 +90,9 @@ class LockEntity(Entity): _attr_changed_by: str | None = None _attr_code_format: str | None = None _attr_is_locked: bool | None = None + _attr_is_locking: bool | None = None + _attr_is_unlocking: bool | None = None + _attr_is_jammed: bool | None = None _attr_state: None = None @property @@ -104,6 +110,21 @@ class LockEntity(Entity): """Return true if the lock is locked.""" return self._attr_is_locked + @property + def is_locking(self) -> bool | None: + """Return true if the lock is locking.""" + return self._attr_is_locking + + @property + def is_unlocking(self) -> bool | None: + """Return true if the lock is unlocking.""" + return self._attr_is_unlocking + + @property + def is_jammed(self) -> bool | None: + """Return true if the lock is jammed (incomplete locking).""" + return self._attr_is_jammed + def lock(self, **kwargs: Any) -> None: """Lock the lock.""" raise NotImplementedError() @@ -143,6 +164,12 @@ class LockEntity(Entity): @property def state(self) -> str | None: """Return the state.""" + if self.is_jammed: + return STATE_JAMMED + if self.is_locking: + return STATE_LOCKING + if self.is_unlocking: + return STATE_UNLOCKING locked = self.is_locked if locked is None: return None diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 3e77a23ffdb..d0829eb742b 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -10,8 +10,11 @@ from homeassistant.const import ( CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE, + STATE_JAMMED, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry @@ -20,7 +23,13 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN -CONDITION_TYPES = {"is_locked", "is_unlocked"} +CONDITION_TYPES = { + "is_locked", + "is_unlocked", + "is_locking", + "is_unlocking", + "is_jammed", +} CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( { @@ -60,7 +69,13 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] == "is_locked": + if config[CONF_TYPE] == "is_jammed": + state = STATE_JAMMED + elif config[CONF_TYPE] == "is_locking": + state = STATE_LOCKING + elif config[CONF_TYPE] == "is_unlocking": + state = STATE_UNLOCKING + elif config[CONF_TYPE] == "is_locked": state = STATE_LOCKED else: state = STATE_UNLOCKED diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 2e96b470893..641030e9f23 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -13,8 +13,11 @@ from homeassistant.const import ( CONF_FOR, CONF_PLATFORM, CONF_TYPE, + STATE_JAMMED, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry @@ -22,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_TYPES = {"locked", "unlocked"} +TRIGGER_TYPES = {"locked", "unlocked", "locking", "unlocking", "jammed"} TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { @@ -74,7 +77,13 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - if config[CONF_TYPE] == "locked": + if config[CONF_TYPE] == "jammed": + to_state = STATE_JAMMED + elif config[CONF_TYPE] == "locking": + to_state = STATE_LOCKING + elif config[CONF_TYPE] == "unlocking": + to_state = STATE_UNLOCKING + elif config[CONF_TYPE] == "locked": to_state = STATE_LOCKED else: to_state = STATE_UNLOCKED diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index ea5cf370af6..cdd538c88be 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -11,7 +11,9 @@ from homeassistant.const import ( SERVICE_LOCK, SERVICE_UNLOCK, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, ) from homeassistant.core import Context, HomeAssistant, State @@ -19,7 +21,7 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED} +VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED, STATE_LOCKING, STATE_UNLOCKING} async def _async_reproduce_state( @@ -48,9 +50,9 @@ async def _async_reproduce_state( service_data = {ATTR_ENTITY_ID: state.entity_id} - if state.state == STATE_LOCKED: + if state.state in {STATE_LOCKED, STATE_LOCKING}: service = SERVICE_LOCK - elif state.state == STATE_UNLOCKED: + elif state.state in {STATE_UNLOCKED, STATE_UNLOCKING}: service = SERVICE_UNLOCK await hass.services.async_call( diff --git a/homeassistant/const.py b/homeassistant/const.py index fb47ef7cb7c..1d754b78b7c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -274,6 +274,9 @@ STATE_ALARM_DISARMING: Final = "disarming" STATE_ALARM_TRIGGERED: Final = "triggered" STATE_LOCKED: Final = "locked" STATE_UNLOCKED: Final = "unlocked" +STATE_LOCKING: Final = "locking" +STATE_UNLOCKING: Final = "unlocking" +STATE_JAMMED: Final = "jammed" STATE_UNAVAILABLE: Final = "unavailable" STATE_OK: Final = "ok" STATE_PROBLEM: Final = "problem" diff --git a/tests/components/demo/test_lock.py b/tests/components/demo/test_lock.py index bf8c0ddb63d..15e4e14524d 100644 --- a/tests/components/demo/test_lock.py +++ b/tests/components/demo/test_lock.py @@ -1,4 +1,6 @@ """The tests for the Demo lock platform.""" +import asyncio + import pytest from homeassistant.components.demo import DOMAIN @@ -7,8 +9,11 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, + STATE_JAMMED, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component @@ -17,6 +22,7 @@ from tests.common import async_mock_service FRONT = "lock.front_door" KITCHEN = "lock.kitchen_door" +POORLY_INSTALLED = "lock.poorly_installed_door" OPENABLE_LOCK = "lock.openable_lock" @@ -35,9 +41,13 @@ async def test_locking(hass): assert state.state == STATE_UNLOCKED await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: KITCHEN}, blocking=True + LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: KITCHEN}, blocking=False ) + await asyncio.sleep(1) + state = hass.states.get(KITCHEN) + assert state.state == STATE_LOCKING + await asyncio.sleep(2) state = hass.states.get(KITCHEN) assert state.state == STATE_LOCKED @@ -48,17 +58,46 @@ async def test_unlocking(hass): assert state.state == STATE_LOCKED await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: FRONT}, blocking=True + LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: FRONT}, blocking=False ) - + await asyncio.sleep(1) + state = hass.states.get(FRONT) + assert state.state == STATE_UNLOCKING + await asyncio.sleep(2) state = hass.states.get(FRONT) assert state.state == STATE_UNLOCKED -async def test_opening(hass): +async def test_jammed_when_locking(hass): + """Test the locking of a lock jams.""" + state = hass.states.get(POORLY_INSTALLED) + assert state.state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: POORLY_INSTALLED}, blocking=False + ) + + await asyncio.sleep(1) + state = hass.states.get(POORLY_INSTALLED) + assert state.state == STATE_LOCKING + await asyncio.sleep(2) + state = hass.states.get(POORLY_INSTALLED) + assert state.state == STATE_JAMMED + + +async def test_opening_mocked(hass): """Test the opening of a lock.""" calls = async_mock_service(hass, LOCK_DOMAIN, SERVICE_OPEN) await hass.services.async_call( LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True ) assert len(calls) == 1 + + +async def test_opening(hass): + """Test the opening of a lock.""" + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True + ) + state = hass.states.get(OPENABLE_LOCK) + assert state.state == STATE_UNLOCKED diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index a8b44511fb2..f7537db18de 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -382,6 +382,13 @@ DEMO_DEVICES = [ "type": "action.devices.types.LOCK", "willReportState": False, }, + { + "id": "lock.poorly_installed_door", + "name": {"name": "Poorly Installed Door"}, + "traits": ["action.devices.traits.LockUnlock"], + "type": "action.devices.types.LOCK", + "willReportState": False, + }, { "id": "alarm_control_panel.alarm", "name": {"name": "Alarm"}, diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index b021ef23391..aeb304cb1c8 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -3,7 +3,13 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, +) from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -60,6 +66,27 @@ async def test_get_conditions(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_unlocking", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_locking", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_jammed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) @@ -110,6 +137,60 @@ async def test_if_state(hass, calls): }, }, }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_unlocking", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_unlocking - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_locking", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_locking - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event5"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_jammed", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_jammed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, ] }, ) @@ -125,3 +206,21 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_unlocked - event - test_event2" + + hass.states.async_set("lock.entity", STATE_UNLOCKING) + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_unlocking - event - test_event3" + + hass.states.async_set("lock.entity", STATE_LOCKING) + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_locking - event - test_event4" + + hass.states.async_set("lock.entity", STATE_JAMMED) + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_jammed - event - test_event5" diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index d4d96927b56..c3539288f94 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -5,7 +5,13 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, +) from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -65,6 +71,27 @@ async def test_get_triggers(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "platform": "device", + "domain": DOMAIN, + "type": "unlocking", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "locking", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "jammed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, expected_triggers) @@ -81,7 +108,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert len(triggers) == 2 + assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger @@ -195,7 +222,82 @@ async def test_if_fires_on_state_change_with_for(hass, calls): ) }, }, - } + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": entity_id, + "type": "unlocking", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": entity_id, + "type": "jammed", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": entity_id, + "type": "locking", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, ] }, ) @@ -214,3 +316,39 @@ async def test_if_fires_on_state_change_with_for(hass, calls): calls[0].data["some"] == f"turn_off device - {entity_id} - unlocked - locked - 0:00:05" ) + + hass.states.async_set(entity_id, STATE_UNLOCKING) + await hass.async_block_till_done() + assert len(calls) == 1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=16)) + await hass.async_block_till_done() + assert len(calls) == 2 + await hass.async_block_till_done() + assert ( + calls[1].data["some"] + == f"turn_on device - {entity_id} - locked - unlocking - 0:00:05" + ) + + hass.states.async_set(entity_id, STATE_JAMMED) + await hass.async_block_till_done() + assert len(calls) == 2 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21)) + await hass.async_block_till_done() + assert len(calls) == 3 + await hass.async_block_till_done() + assert ( + calls[2].data["some"] + == f"turn_off device - {entity_id} - unlocking - jammed - 0:00:05" + ) + + hass.states.async_set(entity_id, STATE_LOCKING) + await hass.async_block_till_done() + assert len(calls) == 3 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=27)) + await hass.async_block_till_done() + assert len(calls) == 4 + await hass.async_block_till_done() + assert ( + calls[3].data["some"] + == f"turn_on device - {entity_id} - jammed - locking - 0:00:05" + ) From 193d1b945b0fad66f5adb0f8476b33a918e3c70c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 18:28:31 +0200 Subject: [PATCH 416/818] Add typing in dynalite and activate mypy (#53238) Co-authored-by: Franck Nijhof --- homeassistant/components/dynalite/__init__.py | 3 ++- homeassistant/components/dynalite/bridge.py | 10 +++++----- homeassistant/components/dynalite/config_flow.py | 9 ++++++--- homeassistant/components/dynalite/convert_config.py | 5 ++++- homeassistant/components/dynalite/dynalitebase.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 703ac1373ba..7dc3d86afe6 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -52,6 +52,7 @@ from .const import ( SERVICE_REQUEST_AREA_PRESET, SERVICE_REQUEST_CHANNEL_LEVEL, ) +from .convert_config import convert_config def num_string(value: int | str) -> str: @@ -263,7 +264,7 @@ async def async_entry_changed(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a bridge from a config entry.""" LOGGER.debug("Setting up entry %s", entry.data) - bridge = DynaliteBridge(hass, entry.data) + bridge = DynaliteBridge(hass, convert_config(entry.data)) # need to do it before the listener hass.data[DOMAIN][entry.entry_id] = bridge entry.async_on_unload(entry.add_update_listener(async_entry_changed)) diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 71cecee8d43..9c911e6983d 100644 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -1,6 +1,7 @@ """Code to handle a Dynalite bridge.""" from __future__ import annotations +from types import MappingProxyType from typing import Any, Callable from dynalite_devices_lib.dynalite_devices import ( @@ -27,9 +28,8 @@ class DynaliteBridge: def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the system based on host parameter.""" self.hass = hass - self.area = {} - self.async_add_devices = {} - self.waiting_devices = {} + self.async_add_devices: dict[str, Callable] = {} + self.waiting_devices: dict[str, list[str]] = {} self.host = config[CONF_HOST] # Configure the dynalite devices self.dynalite_devices = DynaliteDevices( @@ -37,7 +37,7 @@ class DynaliteBridge: update_device_func=self.update_device, notification_func=self.handle_notification, ) - self.dynalite_devices.configure(convert_config(config)) + self.dynalite_devices.configure(config) async def async_setup(self) -> bool: """Set up a Dynalite bridge.""" @@ -45,7 +45,7 @@ class DynaliteBridge: LOGGER.debug("Setting up bridge - host %s", self.host) return await self.dynalite_devices.async_setup() - def reload_config(self, config: dict[str, Any]) -> None: + def reload_config(self, config: MappingProxyType[str, Any]) -> None: """Reconfigure a bridge when config changes.""" LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config) self.dynalite_devices.configure(convert_config(config)) diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index e1d062a6058..d148d09354f 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -8,6 +8,7 @@ from homeassistant.const import CONF_HOST from .bridge import DynaliteBridge from .const import DOMAIN, LOGGER +from .convert_config import convert_config class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -25,11 +26,13 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host = import_info[CONF_HOST] for entry in self._async_current_entries(): if entry.data[CONF_HOST] == host: - if entry.data != import_info: - self.hass.config_entries.async_update_entry(entry, data=import_info) + self.hass.config_entries.async_update_entry( + entry, data=dict(import_info) + ) return self.async_abort(reason="already_configured") + # New entry - bridge = DynaliteBridge(self.hass, import_info) + bridge = DynaliteBridge(self.hass, convert_config(import_info)) if not await bridge.async_setup(): LOGGER.error("Unable to setup bridge - import info=%s", import_info) return self.async_abort(reason="no_connection") diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index 89a7f32b47a..4abc02c0565 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -1,6 +1,7 @@ """Convert the HA config to the dynalite config.""" from __future__ import annotations +from types import MappingProxyType from typing import Any from dynalite_devices_lib import const as dyn_const @@ -136,7 +137,9 @@ def convert_template(config: dict[str, Any]) -> dict[str, Any]: return convert_with_map(config, my_map) -def convert_config(config: dict[str, Any]) -> dict[str, Any]: +def convert_config( + config: dict[str, Any] | MappingProxyType[str, Any] +) -> dict[str, Any]: """Convert a config dict by replacing component consts with library consts.""" my_map = { CONF_NAME: dyn_const.CONF_NAME, diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index ebb1dd23795..56def12afbe 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -43,7 +43,7 @@ class DynaliteBase(Entity): """Initialize the base class.""" self._device = device self._bridge = bridge - self._unsub_dispatchers = [] + self._unsub_dispatchers: list[Callable[[], None]] = [] @property def name(self) -> str: diff --git a/mypy.ini b/mypy.ini index 4bfb8e22fe3..942ef115135 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1139,9 +1139,6 @@ ignore_errors = true [mypy-homeassistant.components.doorbird.*] ignore_errors = true -[mypy-homeassistant.components.dynalite.*] -ignore_errors = true - [mypy-homeassistant.components.edl21.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 720c9157e14..a571047446c 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -38,7 +38,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.dhcp.*", "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", - "homeassistant.components.dynalite.*", "homeassistant.components.edl21.*", "homeassistant.components.elkm1.*", "homeassistant.components.emonitor.*", From 1746103e0e977584f98b46b76c99b5a8b9b7afd9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 20 Jul 2021 18:38:16 +0200 Subject: [PATCH 417/818] Add friendly name to Fritz profile switches (#53190) --- homeassistant/components/fritz/switch.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 1100a480fc8..2969095d34d 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -557,20 +557,14 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity): class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): """Defines a FRITZ!Box Tools DeviceProfile switch.""" + _attr_icon = "mdi:router-wireless-settings" + def __init__(self, fritzbox_tools: FritzBoxTools, device: FritzDevice) -> None: """Init Fritz profile.""" super().__init__(fritzbox_tools, device) self._attr_is_on: bool = False - - @property - def unique_id(self) -> str: - """Return device unique id.""" - return f"{self._mac}_switch" - - @property - def icon(self) -> str: - """Return device icon.""" - return "mdi:router-wireless-settings" + self._name = f"{device.hostname} Internet Access" + self._attr_unique_id = f"{self._mac}_internet_access" async def async_process_update(self) -> None: """Update device.""" From 1ed7b00b7108fc12b36f1bb75eb6bd06ad6483d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Jul 2021 09:39:14 -0700 Subject: [PATCH 418/818] Add last reset and state class to rainforest eagle (#52951) --- .../components/rainforest_eagle/sensor.py | 104 +++++++++--------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index d333f9437f1..53e94d2070e 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -1,5 +1,8 @@ """Support for the Rainforest Eagle-200 energy monitor.""" -from datetime import timedelta +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta import logging from eagle200_reader import EagleReader @@ -7,14 +10,19 @@ from requests.exceptions import ConnectionError as ConnectError, HTTPError, Time from uEagle import Eagle as LegacyReader import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + DEVICE_CLASS_ENERGY, + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( CONF_IP_ADDRESS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt CONF_CLOUD_ID = "cloud_id" CONF_INSTALL_CODE = "install_code" @@ -24,19 +32,43 @@ _LOGGER = logging.getLogger(__name__) MIN_SCAN_INTERVAL = timedelta(seconds=30) + +@dataclass +class SensorType: + """Rainforest sensor type.""" + + name: str + unit_of_measurement: str + device_class: str | None = None + state_class: str | None = None + last_reset: datetime | None = None + + SENSORS = { - "instantanous_demand": ("Eagle-200 Meter Power Demand", POWER_KILO_WATT), - "summation_delivered": ( - "Eagle-200 Total Meter Energy Delivered", - ENERGY_KILO_WATT_HOUR, + "instantanous_demand": SensorType( + name="Eagle-200 Meter Power Demand", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "summation_received": ( - "Eagle-200 Total Meter Energy Received", - ENERGY_KILO_WATT_HOUR, + "summation_delivered": SensorType( + name="Eagle-200 Total Meter Energy Delivered", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), ), - "summation_total": ( - "Eagle-200 Net Meter Energy (Delivered minus Received)", - ENERGY_KILO_WATT_HOUR, + "summation_received": SensorType( + name="Eagle-200 Total Meter Energy Received", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ), + "summation_total": SensorType( + name="Eagle-200 Net Meter Energy (Delivered minus Received)", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, ), } @@ -86,56 +118,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): eagle_data = EagleData(eagle_reader) eagle_data.update() - monitored_conditions = list(SENSORS) - sensors = [] - for condition in monitored_conditions: - sensors.append( - EagleSensor( - eagle_data, condition, SENSORS[condition][0], SENSORS[condition][1] - ) - ) - add_entities(sensors) + add_entities(EagleSensor(eagle_data, condition) for condition in SENSORS) class EagleSensor(SensorEntity): """Implementation of the Rainforest Eagle-200 sensor.""" - def __init__(self, eagle_data, sensor_type, name, unit): + def __init__(self, eagle_data, sensor_type): """Initialize the sensor.""" self.eagle_data = eagle_data self._type = sensor_type - self._name = name - self._unit_of_measurement = unit - self._state = None - - @property - def device_class(self): - """Return the power device class for the instantanous_demand sensor.""" - if self._type == "instantanous_demand": - return DEVICE_CLASS_POWER - - return None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement + sensor_info = SENSORS[sensor_type] + self._attr_name = sensor_info.name + self._attr_unit_of_measurement = sensor_info.unit_of_measurement + self._attr_device_class = sensor_info.device_class + self._attr_state_class = sensor_info.state_class + self._attr_last_reset = sensor_info.last_reset def update(self): """Get the energy information from the Rainforest Eagle.""" self.eagle_data.update() - self._state = self.eagle_data.get_state(self._type) + self._attr_state = self.eagle_data.get_state(self._type) class EagleData: From 165e1917ea6999fd6975fb7e6fd13c383c1414c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 18:57:40 +0200 Subject: [PATCH 419/818] Address late review of Ambiclimate, code clean up (#53231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ambiclimate, code clean up Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/ambiclimate/climate.py Co-authored-by: Franck Nijhof * Update homeassistant/components/ambiclimate/climate.py Co-authored-by: Franck Nijhof * Update homeassistant/components/ambiclimate/config_flow.py Co-authored-by: Franck Nijhof * Update homeassistant/components/ambiclimate/climate.py Co-authored-by: Franck Nijhof * import Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Franck Nijhof --- homeassistant/components/ambiclimate/climate.py | 13 +++++++------ homeassistant/components/ambiclimate/config_flow.py | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index a49253af6bc..8cfebb1bf69 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -1,6 +1,7 @@ """Support for Ambiclimate ac.""" import asyncio import logging +from typing import Any import ambiclimate import voluptuous as vol @@ -146,24 +147,24 @@ class AmbiclimateEntity(ClimateEntity): """Initialize the thermostat.""" self._heater = heater self._store = store - self._attr_unique_id = self._heater.device_id - self._attr_name = self._heater.name + self._attr_unique_id = heater.device_id + self._attr_name = heater.name self._attr_device_info = { "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, "manufacturer": "Ambiclimate", } - self._attr_min_temp = self._heater.get_min_temp() - self._attr_max_temp = self._heater.get_max_temp() + self._attr_min_temp = heater.get_min_temp() + self._attr_max_temp = heater.get_max_temp() - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return await self._heater.set_target_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: await self._heater.turn_on() diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 7f9ff9e5d09..2643b01185a 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Ambiclimate.""" import logging +from aiohttp import web import ambiclimate from homeassistant import config_entries @@ -139,7 +140,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView): url = AUTH_CALLBACK_PATH name = AUTH_CALLBACK_NAME - async def get(self, request) -> str: + async def get(self, request: web.Request) -> str: """Receive authorization token.""" code = request.query.get("code") if code is None: From 6e88428f958f4e8a93e90cdf20f4b57ccecc0aff Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 20 Jul 2021 13:31:55 -0400 Subject: [PATCH 420/818] Fix typing for climacell dataclass (#53240) --- homeassistant/components/climacell/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index f19724b002e..c11c0b1774b 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -158,7 +158,7 @@ class ClimaCellSensorMetadata: name: str unit_imperial: str | None = None unit_metric: str | None = None - metric_conversion: Callable | float = 1.0 + metric_conversion: Callable[[float], float] | float = 1.0 is_metric_check: bool | None = None device_class: str | None = None value_map: IntEnum | None = None From 074d762664976d3414b8a33535e9b850d80a42fa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Jul 2021 20:06:23 +0200 Subject: [PATCH 421/818] Rename and reorganize electric unit constants (#53243) --- homeassistant/components/apcupsd/sensor.py | 36 ++++----- .../components/dsmr_reader/definitions.py | 16 ++-- homeassistant/components/elkm1/sensor.py | 4 +- homeassistant/components/envirophat/sensor.py | 10 +-- homeassistant/components/goalzero/const.py | 10 +-- .../components/greeneye_monitor/sensor.py | 4 +- .../components/growatt_server/sensor.py | 44 +++++------ homeassistant/components/homematic/sensor.py | 4 +- homeassistant/components/isy994/const.py | 4 +- homeassistant/components/juicenet/sensor.py | 8 +- homeassistant/components/keba/sensor.py | 4 +- homeassistant/components/lcn/const.py | 4 +- homeassistant/components/mysensors/sensor.py | 12 +-- homeassistant/components/nut/const.py | 77 ++++++++++++++----- homeassistant/components/onewire/const.py | 8 +- homeassistant/components/rfxtrx/__init__.py | 14 ++-- homeassistant/components/sense/sensor.py | 4 +- homeassistant/components/shelly/sensor.py | 10 +-- homeassistant/components/smappee/sensor.py | 19 +++-- .../components/smartthings/sensor.py | 4 +- .../components/solaredge_local/sensor.py | 24 ++++-- homeassistant/components/solarlog/const.py | 11 ++- homeassistant/components/starline/sensor.py | 4 +- homeassistant/components/subaru/sensor.py | 4 +- .../components/switcher_kis/sensor.py | 4 +- .../components/system_bridge/sensor.py | 4 +- homeassistant/components/tasmota/sensor.py | 12 +-- homeassistant/components/ted5000/sensor.py | 17 +++- homeassistant/components/tibber/sensor.py | 16 ++-- homeassistant/components/wallbox/const.py | 4 +- .../components/wirelesstag/__init__.py | 4 +- homeassistant/const.py | 12 +-- tests/components/onewire/const.py | 10 +-- 33 files changed, 244 insertions(+), 178 deletions(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 4748ae2476e..d30625ee793 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -8,15 +8,15 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_RESOURCES, DEVICE_CLASS_TEMPERATURE, - ELECTRICAL_CURRENT_AMPERE, - ELECTRICAL_VOLT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, FREQUENCY_HERTZ, PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, TEMP_CELSIUS, TIME_MINUTES, TIME_SECONDS, - VOLT, ) import homeassistant.helpers.config_validation as cv @@ -33,7 +33,7 @@ SENSOR_TYPES = { "badbatts": ["Bad Batteries", "", "mdi:information-outline", None], "battdate": ["Battery Replaced", "", "mdi:calendar-clock", None], "battstat": ["Battery Status", "", "mdi:information-outline", None], - "battv": ["Battery Voltage", VOLT, "mdi:flash", None], + "battv": ["Battery Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "bcharge": ["Battery", PERCENTAGE, "mdi:battery", None], "cable": ["Cable Type", "", "mdi:ethernet-cable", None], "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline", None], @@ -46,33 +46,33 @@ SENSOR_TYPES = { "endapc": ["Date and Time", "", "mdi:calendar-clock", None], "extbatts": ["External Batteries", "", "mdi:information-outline", None], "firmware": ["Firmware Version", "", "mdi:information-outline", None], - "hitrans": ["Transfer High", VOLT, "mdi:flash", None], + "hitrans": ["Transfer High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "hostname": ["Hostname", "", "mdi:information-outline", None], "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None], "itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "lastxfer": ["Last Transfer", "", "mdi:transfer", None], "linefail": ["Input Voltage Status", "", "mdi:information-outline", None], "linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline", None], - "linev": ["Input Voltage", VOLT, "mdi:flash", None], + "linev": ["Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "loadpct": ["Load", PERCENTAGE, "mdi:gauge", None], "loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge", None], - "lotrans": ["Transfer Low", VOLT, "mdi:flash", None], + "lotrans": ["Transfer Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "mandate": ["Manufacture Date", "", "mdi:calendar", None], "masterupd": ["Master Update", "", "mdi:information-outline", None], - "maxlinev": ["Input Voltage High", VOLT, "mdi:flash", None], + "maxlinev": ["Input Voltage High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline", None], "mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert", None], - "minlinev": ["Input Voltage Low", VOLT, "mdi:flash", None], + "minlinev": ["Input Voltage Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "mintimel": ["Shutdown Time", "", "mdi:timer-outline", None], "model": ["Model", "", "mdi:information-outline", None], - "nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash", None], - "nominv": ["Nominal Input Voltage", VOLT, "mdi:flash", None], - "nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash", None], + "nombattv": ["Battery Nominal Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], + "nominv": ["Nominal Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], + "nomoutv": ["Nominal Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash", None], - "nomapnt": ["Nominal Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], + "nomapnt": ["Nominal Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None], "numxfers": ["Transfer Count", "", "mdi:counter", None], - "outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash", None], - "outputv": ["Output Voltage", VOLT, "mdi:flash", None], + "outcurnt": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None], + "outputv": ["Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "reg1": ["Register 1 Fault", "", "mdi:information-outline", None], "reg2": ["Register 2 Fault", "", "mdi:information-outline", None], "reg3": ["Register 3 Fault", "", "mdi:information-outline", None], @@ -99,9 +99,9 @@ INFERRED_UNITS = { " Minutes": TIME_MINUTES, " Seconds": TIME_SECONDS, " Percent": PERCENTAGE, - " Volts": VOLT, - " Ampere": ELECTRICAL_CURRENT_AMPERE, - " Volt-Ampere": ELECTRICAL_VOLT_AMPERE, + " Volts": ELECTRIC_POTENTIAL_VOLT, + " Ampere": ELECTRIC_CURRENT_AMPERE, + " Volt-Ampere": POWER_VOLT_AMPERE, " Watts": POWER_WATT, " Hz": FREQUENCY_HERTZ, " C": TEMP_CELSIUS, diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index d403f84e9b9..51aaca24c02 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -7,10 +7,10 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_KILO_WATT, - VOLT, VOLUME_CUBIC_METERS, ) @@ -112,37 +112,37 @@ DEFINITIONS = { "name": "Current voltage L1", "enable_default": True, "device_class": DEVICE_CLASS_VOLTAGE, - "unit": VOLT, + "unit": ELECTRIC_POTENTIAL_VOLT, }, "dsmr/reading/phase_voltage_l2": { "name": "Current voltage L2", "enable_default": True, "device_class": DEVICE_CLASS_VOLTAGE, - "unit": VOLT, + "unit": ELECTRIC_POTENTIAL_VOLT, }, "dsmr/reading/phase_voltage_l3": { "name": "Current voltage L3", "enable_default": True, "device_class": DEVICE_CLASS_VOLTAGE, - "unit": VOLT, + "unit": ELECTRIC_POTENTIAL_VOLT, }, "dsmr/reading/phase_power_current_l1": { "name": "Phase power current L1", "enable_default": True, "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRICAL_CURRENT_AMPERE, + "unit": ELECTRIC_CURRENT_AMPERE, }, "dsmr/reading/phase_power_current_l2": { "name": "Phase power current L2", "enable_default": True, "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRICAL_CURRENT_AMPERE, + "unit": ELECTRIC_CURRENT_AMPERE, }, "dsmr/reading/phase_power_current_l3": { "name": "Phase power current L3", "enable_default": True, "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRICAL_CURRENT_AMPERE, + "unit": ELECTRIC_CURRENT_AMPERE, }, "dsmr/reading/timestamp": { "name": "Telegram timestamp", diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 4a75ccb242e..8f26af545b7 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -9,7 +9,7 @@ from elkm1_lib.util import pretty_const, username import voluptuous as vol from homeassistant.components.sensor import SensorEntity -from homeassistant.const import VOLT +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -255,7 +255,7 @@ class ElkZone(ElkSensor): if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit if self._element.definition == ZoneType.ANALOG_ZONE.value: - return VOLT + return ELECTRIC_POTENTIAL_VOLT return None def _element_changed(self, element, changeset): diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 4e10b5e65d1..9bca552326a 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -10,9 +10,9 @@ from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_POTENTIAL_VOLT, PRESSURE_HPA, TEMP_CELSIUS, - VOLT, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -37,10 +37,10 @@ SENSOR_TYPES = { "magnetometer_z": ["magnetometer_z", " ", "mdi:magnet", None], "temperature": ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "pressure": ["pressure", PRESSURE_HPA, "mdi:gauge", None], - "voltage_0": ["voltage_0", VOLT, "mdi:flash", None], - "voltage_1": ["voltage_1", VOLT, "mdi:flash", None], - "voltage_2": ["voltage_2", VOLT, "mdi:flash", None], - "voltage_3": ["voltage_3", VOLT, "mdi:flash", None], + "voltage_0": ["voltage_0", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], + "voltage_1": ["voltage_1", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], + "voltage_2": ["voltage_2", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], + "voltage_3": ["voltage_3", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index 327a4f4833c..ce4b610b3f0 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -20,7 +20,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -28,7 +29,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_MINUTES, TIME_SECONDS, - VOLT, ) ATTR_DEFAULT_ENABLED = "default_enabled" @@ -66,7 +66,7 @@ SENSOR_DICT = { "ampsIn": { ATTR_NAME: "Amps In", ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_DEFAULT_ENABLED: False, }, @@ -80,7 +80,7 @@ SENSOR_DICT = { "ampsOut": { ATTR_NAME: "Amps Out", ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_DEFAULT_ENABLED: False, }, @@ -101,7 +101,7 @@ SENSOR_DICT = { "volts": { ATTR_NAME: "Volts", ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_UNIT_OF_MEASUREMENT: VOLT, + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, ATTR_DEFAULT_ENABLED: False, }, "socPercent": { diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 0aa106e6801..fac11395c8b 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -5,11 +5,11 @@ from homeassistant.const import ( CONF_SENSOR_TYPE, CONF_TEMPERATURE_UNIT, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_POTENTIAL_VOLT, POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS, - VOLT, ) from . import ( @@ -270,7 +270,7 @@ class VoltageSensor(GEMSensor): """Entity showing voltage.""" _attr_icon = VOLTAGE_ICON - _attr_unit_of_measurement = VOLT + _attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT def __init__(self, monitor_serial_number, number, name): """Construct the entity.""" diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index c8921d9e514..78fd24623d8 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -21,14 +21,14 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, PERCENTAGE, POWER_KILO_WATT, POWER_WATT, TEMP_CELSIUS, - VOLT, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, dt @@ -84,13 +84,13 @@ INVERTER_SENSOR_TYPES = { ), "inverter_voltage_input_1": ( "Input 1 voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vpv1", {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, ), "inverter_amperage_input_1": ( "Input 1 Amperage", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "ipv1", {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, ), @@ -102,13 +102,13 @@ INVERTER_SENSOR_TYPES = { ), "inverter_voltage_input_2": ( "Input 2 voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vpv2", {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, ), "inverter_amperage_input_2": ( "Input 2 Amperage", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "ipv2", {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, ), @@ -120,13 +120,13 @@ INVERTER_SENSOR_TYPES = { ), "inverter_voltage_input_3": ( "Input 3 voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vpv3", {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, ), "inverter_amperage_input_3": ( "Input 3 Amperage", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "ipv3", {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, ), @@ -144,13 +144,13 @@ INVERTER_SENSOR_TYPES = { ), "inverter_reactive_voltage": ( "Reactive voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vacr", {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, ), "inverter_inverter_reactive_amperage": ( "Reactive amperage", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "iacr", {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, ), @@ -280,13 +280,13 @@ STORAGE_SENSOR_TYPES = { ), "storage_grid_voltage": ( "AC input voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vGrid", {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, ), "storage_pv_charging_voltage": ( "PV charging voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vpv", {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, ), @@ -298,7 +298,7 @@ STORAGE_SENSOR_TYPES = { ), "storage_output_voltage": ( "Output voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "outPutVolt", {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, ), @@ -310,31 +310,31 @@ STORAGE_SENSOR_TYPES = { ), "storage_current_PV": ( "Solar charge current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "iAcCharge", {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, ), "storage_current_1": ( "Solar current to storage", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "iChargePV1", {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, ), "storage_grid_amperage_input": ( "Grid charge current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "chgCurr", {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, ), "storage_grid_out_current": ( "Grid out current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "outPutCurrent", {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, ), "storage_battery_voltage": ( "Battery voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vBat", {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, ), @@ -398,19 +398,19 @@ MIX_SENSOR_TYPES = { ), "mix_battery_voltage": ( "Battery voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vbat", {"device_class": DEVICE_CLASS_VOLTAGE}, ), "mix_pv1_voltage": ( "PV1 voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vpv1", {"device_class": DEVICE_CLASS_VOLTAGE}, ), "mix_pv2_voltage": ( "PV2 voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vpv2", {"device_class": DEVICE_CLASS_VOLTAGE}, ), @@ -490,7 +490,7 @@ MIX_SENSOR_TYPES = { ), "mix_grid_voltage": ( "Grid voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "vAc1", {"device_class": DEVICE_CLASS_VOLTAGE}, ), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 2bc51f67896..ad62001d5f9 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ELECTRIC_CURRENT_MILLIAMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_MILLIMETERS, @@ -18,7 +19,6 @@ from homeassistant.const import ( PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - VOLT, VOLUME_CUBIC_METERS, ) @@ -49,7 +49,7 @@ HM_UNIT_HA_CAST = { "BRIGHTNESS": "#", "POWER": POWER_WATT, "CURRENT": ELECTRIC_CURRENT_MILLIAMPERE, - "VOLTAGE": VOLT, + "VOLTAGE": ELECTRIC_POTENTIAL_VOLT, "ENERGY_COUNTER": ENERGY_WATT_HOUR, "GAS_POWER": VOLUME_CUBIC_METERS, "GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 03b1fa6c66b..343f01332f2 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -50,6 +50,7 @@ from homeassistant.const import ( DEGREE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, @@ -102,7 +103,6 @@ from homeassistant.const import ( TIME_SECONDS, TIME_YEARS, UV_INDEX, - VOLT, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE, @@ -399,7 +399,7 @@ UOM_FRIENDLY_NAME = { "65": "SML", "69": VOLUME_GALLONS, "71": UV_INDEX, - "72": VOLT, + "72": ELECTRIC_POTENTIAL_VOLT, "73": POWER_WATT, "74": IRRADIATION_WATTS_PER_SQUARE_METER, "75": "weekday", diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 81fabf17eea..51792daf38c 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -6,12 +6,12 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, TIME_SECONDS, - VOLT, ) from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR @@ -25,10 +25,10 @@ SENSOR_TYPES = { DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, ], - "voltage": ["Voltage", VOLT, DEVICE_CLASS_VOLTAGE, None], + "voltage": ["Voltage", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, None], "amps": [ "Amps", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, ], diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 836785490e8..2792246d71c 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_POWER, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, ) @@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Max Current", "max_current", "mdi:flash", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, ), KebaSensor( keba, diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 3458c78f853..faef86dc70a 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -3,11 +3,11 @@ from itertools import product from homeassistant.const import ( DEGREE, + ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, - VOLT, ) PLATFORMS = ["binary_sensor", "climate", "cover", "light", "scene", "sensor", "switch"] @@ -171,7 +171,7 @@ VAR_UNITS = [ "PERCENT", "PPM", "VOLT", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "AMPERE", "AMP", "A", diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index f16f9e09c41..f2908567a14 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -11,20 +11,20 @@ from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, - ELECTRICAL_CURRENT_AMPERE, - ELECTRICAL_VOLT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_METERS, LIGHT_LUX, MASS_KILOGRAMS, PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, SOUND_PRESSURE_DB, TEMP_CELSIUS, TEMP_FAHRENHEIT, - VOLT, VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant @@ -59,13 +59,13 @@ SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { "S_VIBRATION": [FREQUENCY_HERTZ, None, None], "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny", None], }, - "V_VOLTAGE": [VOLT, "mdi:flash", None], - "V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto", None], + "V_VOLTAGE": [ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], + "V_CURRENT": [ELECTRIC_CURRENT_AMPERE, "mdi:flash-auto", None], "V_PH": ["pH", None, None], "V_ORP": [ELECTRIC_POTENTIAL_MILLIVOLT, None, None], "V_EC": [CONDUCTIVITY, None, None], "V_VAR": ["var", None, None], - "V_VA": [ELECTRICAL_VOLT_AMPERE, None, None], + "V_VA": [POWER_VOLT_AMPERE, None, None], } diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 890ac3697dd..1f5fecdd219 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -7,14 +7,14 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_VOLTAGE, ) from homeassistant.const import ( - ELECTRICAL_CURRENT_AMPERE, - ELECTRICAL_VOLT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, FREQUENCY_HERTZ, PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, TEMP_CELSIUS, TIME_SECONDS, - VOLT, ) DOMAIN = "nut" @@ -80,8 +80,8 @@ SENSOR_TYPES = { "ups.display.language": ["Language", "", "mdi:information-outline", None], "ups.contacts": ["External Contacts", "", "mdi:information-outline", None], "ups.efficiency": ["Efficiency", PERCENTAGE, "mdi:gauge", None], - "ups.power": ["Current Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], - "ups.power.nominal": ["Nominal Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], + "ups.power": ["Current Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None], + "ups.power.nominal": ["Nominal Power", POWER_VOLT_AMPERE, "mdi:flash", None], "ups.realpower": [ "Current Real Power", POWER_WATT, @@ -121,25 +121,40 @@ SENSOR_TYPES = { None, ], "battery.charger.status": ["Charging Status", "", "mdi:information-outline", None], - "battery.voltage": ["Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], - "battery.voltage.nominal": [ - "Nominal Battery Voltage", - VOLT, + "battery.voltage": [ + "Battery Voltage", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], + "battery.voltage.nominal": [ + "Nominal Battery Voltage", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], + "battery.voltage.low": [ + "Low Battery Voltage", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], + "battery.voltage.high": [ + "High Battery Voltage", + ELECTRIC_POTENTIAL_VOLT, None, DEVICE_CLASS_VOLTAGE, ], - "battery.voltage.low": ["Low Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], - "battery.voltage.high": ["High Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash", None], "battery.current": [ "Battery Current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "mdi:flash", None, ], "battery.current.total": [ "Total Battery Current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "mdi:flash", None, ], @@ -184,18 +199,33 @@ SENSOR_TYPES = { "mdi:information-outline", None, ], - "input.transfer.low": ["Low Voltage Transfer", VOLT, None, DEVICE_CLASS_VOLTAGE], - "input.transfer.high": ["High Voltage Transfer", VOLT, None, DEVICE_CLASS_VOLTAGE], + "input.transfer.low": [ + "Low Voltage Transfer", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], + "input.transfer.high": [ + "High Voltage Transfer", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], "input.transfer.reason": [ "Voltage Transfer Reason", "", "mdi:information-outline", None, ], - "input.voltage": ["Input Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], + "input.voltage": [ + "Input Voltage", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], "input.voltage.nominal": [ "Nominal Input Voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, None, DEVICE_CLASS_VOLTAGE, ], @@ -212,17 +242,22 @@ SENSOR_TYPES = { "mdi:information-outline", None, ], - "output.current": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash", None], + "output.current": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None], "output.current.nominal": [ "Nominal Output Current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "mdi:flash", None, ], - "output.voltage": ["Output Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], + "output.voltage": [ + "Output Voltage", + ELECTRIC_POTENTIAL_VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], "output.voltage.nominal": [ "Nominal Output Voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, None, DEVICE_CLASS_VOLTAGE, ], diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index d2c712c26c5..9112bf5e8f6 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -11,12 +11,12 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, TEMP_CELSIUS, - VOLT, ) CONF_MOUNT_DIR = "mount_dir" @@ -55,8 +55,8 @@ SENSOR_TYPES: dict[str, list[str | None]] = { SENSOR_TYPE_WETNESS: [PERCENTAGE, DEVICE_CLASS_HUMIDITY], SENSOR_TYPE_MOISTURE: [PRESSURE_CBAR, DEVICE_CLASS_PRESSURE], SENSOR_TYPE_COUNT: ["count", None], - SENSOR_TYPE_VOLTAGE: [VOLT, DEVICE_CLASS_VOLTAGE], - SENSOR_TYPE_CURRENT: [ELECTRICAL_CURRENT_AMPERE, DEVICE_CLASS_CURRENT], + SENSOR_TYPE_VOLTAGE: [ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE], + SENSOR_TYPE_CURRENT: [ELECTRIC_CURRENT_AMPERE, DEVICE_CLASS_CURRENT], SENSOR_TYPE_SENSED: [None, None], SWITCH_TYPE_LATCH: [None, None], SWITCH_TYPE_PIO: [None, None], diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index a4be36df998..66d4235ffdb 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -23,7 +23,8 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, DEGREE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_STOP, LENGTH_MILLIMETERS, @@ -35,7 +36,6 @@ from homeassistant.const import ( SPEED_METERS_PER_SECOND, TEMP_CELSIUS, UV_INDEX, - VOLT, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -87,11 +87,11 @@ DATA_TYPES = OrderedDict( ("Wind gust", SPEED_METERS_PER_SECOND), ("Chill", TEMP_CELSIUS), ("Count", "count"), - ("Current Ch. 1", ELECTRICAL_CURRENT_AMPERE), - ("Current Ch. 2", ELECTRICAL_CURRENT_AMPERE), - ("Current Ch. 3", ELECTRICAL_CURRENT_AMPERE), - ("Voltage", VOLT), - ("Current", ELECTRICAL_CURRENT_AMPERE), + ("Current Ch. 1", ELECTRIC_CURRENT_AMPERE), + ("Current Ch. 2", ELECTRIC_CURRENT_AMPERE), + ("Current Ch. 3", ELECTRIC_CURRENT_AMPERE), + ("Voltage", ELECTRIC_POTENTIAL_VOLT), + ("Current", ELECTRIC_CURRENT_AMPERE), ("Battery numeric", PERCENTAGE), ("Rssi numeric", SIGNAL_STRENGTH_DECIBELS_MILLIWATT), ] diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 238b0b83cde..dd522d012a5 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -3,9 +3,9 @@ from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntit from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_POWER, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, - VOLT, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -174,7 +174,7 @@ class SenseActiveSensor(SensorEntity): class SenseVoltageSensor(SensorEntity): """Implementation of a Sense energy voltage sensor.""" - _attr_unit_of_measurement = VOLT + _attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index a42f38f8a1b..8a435c3e50f 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -4,13 +4,13 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEGREE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - VOLT, ) from .const import SHAIR_MAX_WORK_HOURS @@ -43,7 +43,7 @@ SENSORS = { ), ("emeter", "current"): BlockAttributeDescription( name="Current", - unit=ELECTRICAL_CURRENT_AMPERE, + unit=ELECTRIC_CURRENT_AMPERE, value=lambda value: value, device_class=sensor.DEVICE_CLASS_CURRENT, state_class=sensor.STATE_CLASS_MEASUREMENT, @@ -72,7 +72,7 @@ SENSORS = { ), ("emeter", "voltage"): BlockAttributeDescription( name="Voltage", - unit=VOLT, + unit=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_VOLTAGE, state_class=sensor.STATE_CLASS_MEASUREMENT, @@ -186,7 +186,7 @@ SENSORS = { ), ("adc", "adc"): BlockAttributeDescription( name="ADC", - unit=VOLT, + unit=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_VOLTAGE, state_class=sensor.STATE_CLASS_MEASUREMENT, diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 75c5da85c34..fb00886f1f6 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -1,6 +1,11 @@ """Support for monitoring a Smappee energy sensor.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_WATT_HOUR, POWER_WATT, VOLT +from homeassistant.const import ( + DEVICE_CLASS_POWER, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_WATT_HOUR, + POWER_WATT, +) from .const import DOMAIN @@ -93,7 +98,7 @@ VOLTAGE_SENSORS = { "phase_voltages_a": [ "Phase voltages - A", "mdi:flash", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "phase_voltage_a", None, ["ONE", "TWO", "THREE_STAR", "THREE_DELTA"], @@ -101,7 +106,7 @@ VOLTAGE_SENSORS = { "phase_voltages_b": [ "Phase voltages - B", "mdi:flash", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "phase_voltage_b", None, ["TWO", "THREE_STAR", "THREE_DELTA"], @@ -109,7 +114,7 @@ VOLTAGE_SENSORS = { "phase_voltages_c": [ "Phase voltages - C", "mdi:flash", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "phase_voltage_c", None, ["THREE_STAR"], @@ -117,7 +122,7 @@ VOLTAGE_SENSORS = { "line_voltages_a": [ "Line voltages - A", "mdi:flash", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "line_voltage_a", None, ["ONE", "TWO", "THREE_STAR", "THREE_DELTA"], @@ -125,7 +130,7 @@ VOLTAGE_SENSORS = { "line_voltages_b": [ "Line voltages - B", "mdi:flash", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "line_voltage_b", None, ["TWO", "THREE_STAR", "THREE_DELTA"], @@ -133,7 +138,7 @@ VOLTAGE_SENSORS = { "line_voltages_c": [ "Line voltages - C", "mdi:flash", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "line_voltage_c", None, ["THREE_STAR", "THREE_DELTA"], diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index a7e2926036c..b8bb071fc0a 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, MASS_KILOGRAMS, @@ -22,7 +23,6 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - VOLT, VOLUME_CUBIC_METERS, ) @@ -254,7 +254,7 @@ CAPABILITY_TO_SENSORS = { Map(Attribute.ultraviolet_index, "Ultraviolet Index", None, None) ], Capability.voltage_measurement: [ - Map(Attribute.voltage, "Voltage Measurement", VOLT, None) + Map(Attribute.voltage, "Voltage Measurement", ELECTRIC_POTENTIAL_VOLT, None) ], Capability.washer_mode: [Map(Attribute.washer_mode, "Washer Mode", None, None)], Capability.washer_operating_state: [ diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 920cbb564f8..3f159ce4480 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -14,13 +14,13 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, DEVICE_CLASS_TEMPERATURE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - VOLT, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -44,8 +44,20 @@ INVERTER_MODES = ( # Supported sensor types: # Key: ['json_key', 'name', unit, icon, attribute name] SENSOR_TYPES = { - "current_AC_voltage": ["gridvoltage", "Grid Voltage", VOLT, "mdi:current-ac", None], - "current_DC_voltage": ["dcvoltage", "DC Voltage", VOLT, "mdi:current-dc", None], + "current_AC_voltage": [ + "gridvoltage", + "Grid Voltage", + ELECTRIC_POTENTIAL_VOLT, + "mdi:current-ac", + None, + ], + "current_DC_voltage": [ + "dcvoltage", + "DC Voltage", + ELECTRIC_POTENTIAL_VOLT, + "mdi:current-dc", + None, + ], "current_frequency": [ "gridfrequency", "Grid Frequency", @@ -113,7 +125,7 @@ SENSOR_TYPES = { "optimizer_current": [ "optimizercurrent", "Average Optimizer Current", - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, "mdi:solar-panel", None, None, @@ -137,7 +149,7 @@ SENSOR_TYPES = { "optimizer_voltage": [ "optimizervoltage", "Average Optimizer Voltage", - VOLT, + ELECTRIC_POTENTIAL_VOLT, "mdi:solar-panel", None, None, diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index dab844f86d6..0d989642d07 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,7 +1,12 @@ """Constants for the Solar-Log integration.""" from datetime import timedelta -from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, VOLT +from homeassistant.const import ( + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) DOMAIN = "solarlog" @@ -17,8 +22,8 @@ SENSOR_TYPES = { "time": ["TIME", "last update", None, "mdi:calendar-clock"], "power_ac": ["powerAC", "power AC", POWER_WATT, "mdi:solar-power"], "power_dc": ["powerDC", "power DC", POWER_WATT, "mdi:solar-power"], - "voltage_ac": ["voltageAC", "voltage AC", VOLT, "mdi:flash"], - "voltage_dc": ["voltageDC", "voltage DC", VOLT, "mdi:flash"], + "voltage_ac": ["voltageAC", "voltage AC", ELECTRIC_POTENTIAL_VOLT, "mdi:flash"], + "voltage_dc": ["voltageDC", "voltage DC", ELECTRIC_POTENTIAL_VOLT, "mdi:flash"], "yield_day": ["yieldDAY", "yield day", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], "yield_yesterday": [ "yieldYESTERDAY", diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 4cb470be894..e7996befad3 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,10 +1,10 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity from homeassistant.const import ( + ELECTRIC_POTENTIAL_VOLT, LENGTH_KILOMETERS, PERCENTAGE, TEMP_CELSIUS, - VOLT, VOLUME_LITERS, ) from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level @@ -14,7 +14,7 @@ from .const import DOMAIN from .entity import StarlineEntity SENSOR_TYPES = { - "battery": ["Battery", None, VOLT, None], + "battery": ["Battery", None, ELECTRIC_POTENTIAL_VOLT, None], "balance": ["Balance", None, None, "mdi:cash-multiple"], "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 3994c9c6124..ff1d8b715d7 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -8,13 +8,13 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, TIME_MINUTES, - VOLT, VOLUME_GALLONS, VOLUME_LITERS, ) @@ -111,7 +111,7 @@ API_GEN_2_SENSORS = [ SENSOR_TYPE: "12V Battery Voltage", SENSOR_CLASS: DEVICE_CLASS_VOLTAGE, SENSOR_FIELD: sc.BATTERY_VOLTAGE, - SENSOR_UNITS: VOLT, + SENSOR_UNITS: ELECTRIC_POTENTIAL_VOLT, }, ] diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 58a32e69154..705c6f0a2b6 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT +from homeassistant.const import ELECTRIC_CURRENT_AMPERE, POWER_WATT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -45,7 +45,7 @@ POWER_SENSORS = { ), "electric_current": AttributeDescription( name="Electric Current", - unit=ELECTRICAL_CURRENT_AMPERE, + unit=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index d73d85fca1f..71bb7030a11 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -14,10 +14,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, FREQUENCY_GIGAHERTZ, PERCENTAGE, TEMP_CELSIUS, - VOLT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -205,7 +205,7 @@ class BridgeCpuVoltageSensor(BridgeSensor): "CPU Voltage", None, DEVICE_CLASS_VOLTAGE, - VOLT, + ELECTRIC_POTENTIAL_VOLT, False, ) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 87b81322799..b756d656921 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -26,14 +26,15 @@ from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, - ELECTRICAL_CURRENT_AMPERE, - ELECTRICAL_VOLT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_CENTIMETERS, LIGHT_LUX, MASS_KILOGRAMS, PERCENTAGE, + POWER_VOLT_AMPERE, POWER_WATT, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS, @@ -44,7 +45,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, - VOLT, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -134,8 +134,8 @@ SENSOR_UNIT_MAP = { hc.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, hc.CONCENTRATION_PARTS_PER_BILLION: CONCENTRATION_PARTS_PER_BILLION, hc.CONCENTRATION_PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION, - hc.ELECTRICAL_CURRENT_AMPERE: ELECTRICAL_CURRENT_AMPERE, - hc.ELECTRICAL_VOLT_AMPERE: ELECTRICAL_VOLT_AMPERE, + hc.ELECTRICAL_CURRENT_AMPERE: ELECTRIC_CURRENT_AMPERE, + hc.ELECTRICAL_VOLT_AMPERE: POWER_VOLT_AMPERE, hc.ENERGY_KILO_WATT_HOUR: ENERGY_KILO_WATT_HOUR, hc.FREQUENCY_HERTZ: FREQUENCY_HERTZ, hc.LENGTH_CENTIMETERS: LENGTH_CENTIMETERS, @@ -152,7 +152,7 @@ SENSOR_UNIT_MAP = { hc.TEMP_CELSIUS: TEMP_CELSIUS, hc.TEMP_FAHRENHEIT: TEMP_FAHRENHEIT, hc.TEMP_KELVIN: TEMP_KELVIN, - hc.VOLT: VOLT, + hc.VOLT: ELECTRIC_POTENTIAL_VOLT, } diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 5c439651ed5..6732014c747 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -12,7 +12,13 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT, VOLT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + ELECTRIC_POTENTIAL_VOLT, + POWER_WATT, +) from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle @@ -47,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for mtu in gateway.data: dev.append(Ted5000Sensor(gateway, name, mtu, POWER_WATT)) - dev.append(Ted5000Sensor(gateway, name, mtu, VOLT)) + dev.append(Ted5000Sensor(gateway, name, mtu, ELECTRIC_POTENTIAL_VOLT)) add_entities(dev) return True @@ -60,7 +66,7 @@ class Ted5000Sensor(SensorEntity): def __init__(self, gateway, name, mtu, unit): """Initialize the sensor.""" - units = {POWER_WATT: "power", VOLT: "voltage"} + units = {POWER_WATT: "power", ELECTRIC_POTENTIAL_VOLT: "voltage"} self._gateway = gateway self._name = f"{name} mtu{mtu} {units[unit]}" self._mtu = mtu @@ -112,4 +118,7 @@ class Ted5000Gateway: power = int(doc["LiveData"]["Power"]["MTU%d" % mtu]["PowerNow"]) voltage = int(doc["LiveData"]["Voltage"]["MTU%d" % mtu]["VoltageNow"]) - self.data[mtu] = {POWER_WATT: power, VOLT: voltage / 10} + self.data[mtu] = { + POWER_WATT: power, + ELECTRIC_POTENTIAL_VOLT: voltage / 10, + } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 65f2f5e17a1..9ba175db57d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -22,12 +22,12 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ( - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, - VOLT, ) from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady @@ -126,37 +126,37 @@ RT_SENSOR_MAP: dict[str, TibberSensorMetadata] = { "voltagePhase1": TibberSensorMetadata( "voltage phase1", DEVICE_CLASS_VOLTAGE, - VOLT, + ELECTRIC_POTENTIAL_VOLT, STATE_CLASS_MEASUREMENT, ), "voltagePhase2": TibberSensorMetadata( "voltage phase2", DEVICE_CLASS_VOLTAGE, - VOLT, + ELECTRIC_POTENTIAL_VOLT, STATE_CLASS_MEASUREMENT, ), "voltagePhase3": TibberSensorMetadata( "voltage phase3", DEVICE_CLASS_VOLTAGE, - VOLT, + ELECTRIC_POTENTIAL_VOLT, STATE_CLASS_MEASUREMENT, ), "currentL1": TibberSensorMetadata( "current L1", DEVICE_CLASS_CURRENT, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT, ), "currentL2": TibberSensorMetadata( "current L2", DEVICE_CLASS_CURRENT, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT, ), "currentL3": TibberSensorMetadata( "current L3", DEVICE_CLASS_CURRENT, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT, ), "signalStrength": TibberSensorMetadata( diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 41996107ce0..c8044f6990a 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,7 @@ from homeassistant.const import ( CONF_ICON, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, @@ -30,7 +30,7 @@ CONF_SENSOR_TYPES = { CONF_ICON: "mdi:ev-station", CONF_NAME: "Max Available Power", CONF_ROUND: 0, - CONF_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + CONF_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, STATE_UNAVAILABLE: False, }, "charging_speed": { diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 5da19f54dcf..4e3ace38411 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -11,9 +11,9 @@ from homeassistant.const import ( ATTR_VOLTAGE, CONF_PASSWORD, CONF_USERNAME, + ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - VOLT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -276,7 +276,7 @@ class WirelessTagBaseSensor(Entity): """Return the state attributes.""" return { ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100), - ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}{VOLT}", + ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}{ELECTRIC_POTENTIAL_VOLT}", ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}{SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}{PERCENTAGE}", diff --git a/homeassistant/const.py b/homeassistant/const.py index 1d754b78b7c..13b94b799db 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -401,19 +401,19 @@ ATTR_TEMPERATURE: Final = "temperature" # Power units POWER_WATT: Final = "W" POWER_KILO_WATT: Final = "kW" - -# Voltage units -VOLT: Final = "V" +POWER_VOLT_AMPERE: Final = "VA" # Energy units ENERGY_WATT_HOUR: Final = "Wh" ENERGY_KILO_WATT_HOUR: Final = "kWh" -# Electrical units +# Electric_current units ELECTRIC_CURRENT_MILLIAMPERE: Final = "mA" -ELECTRICAL_CURRENT_AMPERE: Final = "A" -ELECTRICAL_VOLT_AMPERE: Final = "VA" +ELECTRIC_CURRENT_AMPERE: Final = "A" + +# Electric_potential units ELECTRIC_POTENTIAL_MILLIVOLT: Final = "mV" +ELECTRIC_POTENTIAL_VOLT: Final = "V" # Degree units DEGREE: Final = "°" diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 1eb2b4b390a..5c12571fc1e 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -14,14 +14,14 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ELECTRICAL_CURRENT_AMPERE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, STATE_OFF, STATE_ON, TEMP_CELSIUS, - VOLT, ) MOCK_OWPROXY_DEVICES = { @@ -347,7 +347,7 @@ MOCK_OWPROXY_DEVICES = { "unique_id": "/26.111111111111/VAD", "injected_value": b" 2.97", "result": "3.0", - "unit": VOLT, + "unit": ELECTRIC_POTENTIAL_VOLT, "class": DEVICE_CLASS_VOLTAGE, "disabled": True, }, @@ -356,7 +356,7 @@ MOCK_OWPROXY_DEVICES = { "unique_id": "/26.111111111111/VDD", "injected_value": b" 4.74", "result": "4.7", - "unit": VOLT, + "unit": ELECTRIC_POTENTIAL_VOLT, "class": DEVICE_CLASS_VOLTAGE, "disabled": True, }, @@ -365,7 +365,7 @@ MOCK_OWPROXY_DEVICES = { "unique_id": "/26.111111111111/IAD", "injected_value": b" 1", "result": "1.0", - "unit": ELECTRICAL_CURRENT_AMPERE, + "unit": ELECTRIC_CURRENT_AMPERE, "class": DEVICE_CLASS_CURRENT, "disabled": True, }, From a05392fbf287c95fdd6d83ef0d536ead147bf555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 20:07:06 +0200 Subject: [PATCH 422/818] Tibber, remove yaml support (#53235) --- homeassistant/components/tibber/__init__.py | 26 +++---------------- .../components/tibber/config_flow.py | 4 --- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index d575a520cb2..a18bb855f8f 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -4,10 +4,8 @@ import logging import aiohttp import tibber -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_ACCESS_TOKEN, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -20,13 +18,7 @@ PLATFORMS = [ "sensor", ] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -35,18 +27,6 @@ async def async_setup(hass, config): """Set up the Tibber component.""" hass.data[DATA_HASS_CONFIG] = config - - if DOMAIN not in config: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) - return True @@ -82,7 +62,7 @@ async def async_setup_entry(hass, entry): # have to use discovery to load platform. hass.async_create_task( discovery.async_load_platform( - hass, "notify", DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DATA_HASS_CONFIG] + hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG] ) ) return True diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index 1c1cef88776..4e804225c56 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -19,10 +19,6 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, import_info): - """Set the config entry up from yaml.""" - return await self.async_step_user(import_info) - async def async_step_user(self, user_input=None): """Handle the initial step.""" From 6be30b0289371bf71915ae31246ef37d7d34a45e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Jul 2021 20:08:39 +0200 Subject: [PATCH 423/818] Use unit constants (#53244) * Powerwall - use POWER_KILO_WATT constant * Use constants firtz sensor --- homeassistant/components/fritz/sensor.py | 22 ++++++++++++-------- homeassistant/components/powerwall/const.py | 2 -- homeassistant/components/powerwall/sensor.py | 10 ++++++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 5820be138b4..031f7bc555c 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -10,7 +10,11 @@ from fritzconnection.lib.fritzstatus import FritzStatus from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ( + DATA_GIGABYTES, + DATA_RATE_KILOBYTES_PER_SECOND, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow @@ -97,38 +101,38 @@ SENSOR_DATA = { "kb_s_sent": SensorData( name="kB/s sent", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement="kB/s", + unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_kb_s_sent_state, ), "kb_s_received": SensorData( name="kB/s received", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement="kB/s", + unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:download", state_provider=_retrieve_kb_s_received_state, ), "max_kb_s_sent": SensorData( - name="Max kb/s sent", - unit_of_measurement="kb/s", + name="Max kB/s sent", + unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_max_kb_s_sent_state, ), "max_kb_s_received": SensorData( - name="Max kb/s received", - unit_of_measurement="kb/s", + name="Max kB/s received", + unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:download", state_provider=_retrieve_max_kb_s_received_state, ), "gb_sent": SensorData( name="GB sent", - unit_of_measurement="GB", + unit_of_measurement=DATA_GIGABYTES, icon="mdi:upload", state_provider=_retrieve_gb_sent_state, ), "gb_received": SensorData( name="GB received", - unit_of_measurement="GB", + unit_of_measurement=DATA_GIGABYTES, icon="mdi:download", state_provider=_retrieve_gb_received_state, ), diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index f338d5f981d..c86333cb9f8 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -32,5 +32,3 @@ POWERWALL_HTTP_SESSION = "http_session" MODEL = "PowerWall 2" MANUFACTURER = "Tesla" - -ENERGY_KILO_WATT = "kW" diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index d6c326593aa..d536c776bf0 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -4,7 +4,12 @@ import logging from tesla_powerwall import MeterType from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, PERCENTAGE +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_POWER, + PERCENTAGE, + POWER_KILO_WATT, +) from .const import ( ATTR_ENERGY_EXPORTED, @@ -14,7 +19,6 @@ from .const import ( ATTR_INSTANT_TOTAL_CURRENT, ATTR_IS_ACTIVE, DOMAIN, - ENERGY_KILO_WATT, POWERWALL_API_CHARGE, POWERWALL_API_DEVICE_TYPE, POWERWALL_API_METERS, @@ -83,7 +87,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = ENERGY_KILO_WATT + _attr_unit_of_measurement = POWER_KILO_WATT _attr_device_class = DEVICE_CLASS_POWER def __init__( From a14bde8187016bceba77abf100821e36bc733da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 20 Jul 2021 20:18:09 +0200 Subject: [PATCH 424/818] Melcloud use NamedTuple (#53234) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/melcloud/sensor.py | 188 ++++++++++---------- 1 file changed, 98 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index c1b7e5e8cbd..b0f1d73f238 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -1,4 +1,8 @@ """Support for MelCloud device sensors.""" +from __future__ import annotations + +from typing import Any, Callable, NamedTuple + from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW from pymelcloud.atw_device import Zone @@ -8,83 +12,85 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ENERGY_KILO_WATT_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS from homeassistant.util import dt as dt_util from . import MelCloudDevice from .const import DOMAIN -ATTR_MEASUREMENT_NAME = "measurement_name" -ATTR_UNIT = "unit" -ATTR_VALUE_FN = "value_fn" -ATTR_ENABLED_FN = "enabled" -ATA_SENSORS = { - "room_temperature": { - ATTR_MEASUREMENT_NAME: "Room Temperature", - ATTR_ICON: "mdi:thermometer", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_VALUE_FN: lambda x: x.device.room_temperature, - ATTR_ENABLED_FN: lambda x: True, - }, - "energy": { - ATTR_MEASUREMENT_NAME: "Energy", - ATTR_ICON: "mdi:factory", - ATTR_UNIT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed, - ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter, - }, +class SensorMetadata(NamedTuple): + """Metadata for an individual sensor.""" + + measurement_name: str + icon: str + unit: str + device_class: str + value_fn: Callable[[Any], float] + enabled: Callable[[Any], bool] + + +ATA_SENSORS: dict[str, SensorMetadata] = { + "room_temperature": SensorMetadata( + "Room Temperature", + icon="mdi:thermometer", + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + value_fn=lambda x: x.device.room_temperature, + enabled=lambda x: True, + ), + "energy": SensorMetadata( + "Energy", + icon="mdi:factory", + unit=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + value_fn=lambda x: x.device.total_energy_consumed, + enabled=lambda x: x.device.has_energy_consumed_meter, + ), } -ATW_SENSORS = { - "outside_temperature": { - ATTR_MEASUREMENT_NAME: "Outside Temperature", - ATTR_ICON: "mdi:thermometer", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_VALUE_FN: lambda x: x.device.outside_temperature, - ATTR_ENABLED_FN: lambda x: True, - }, - "tank_temperature": { - ATTR_MEASUREMENT_NAME: "Tank Temperature", - ATTR_ICON: "mdi:thermometer", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_VALUE_FN: lambda x: x.device.tank_temperature, - ATTR_ENABLED_FN: lambda x: True, - }, +ATW_SENSORS: dict[str, SensorMetadata] = { + "outside_temperature": SensorMetadata( + "Outside Temperature", + icon="mdi:thermometer", + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + value_fn=lambda x: x.device.outside_temperature, + enabled=lambda x: True, + ), + "tank_temperature": SensorMetadata( + "Tank Temperature", + icon="mdi:thermometer", + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + value_fn=lambda x: x.device.tank_temperature, + enabled=lambda x: True, + ), } -ATW_ZONE_SENSORS = { - "room_temperature": { - ATTR_MEASUREMENT_NAME: "Room Temperature", - ATTR_ICON: "mdi:thermometer", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_VALUE_FN: lambda zone: zone.room_temperature, - ATTR_ENABLED_FN: lambda x: True, - }, - "flow_temperature": { - ATTR_MEASUREMENT_NAME: "Flow Temperature", - ATTR_ICON: "mdi:thermometer", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_VALUE_FN: lambda zone: zone.flow_temperature, - ATTR_ENABLED_FN: lambda x: True, - }, - "return_temperature": { - ATTR_MEASUREMENT_NAME: "Flow Return Temperature", - ATTR_ICON: "mdi:thermometer", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_VALUE_FN: lambda zone: zone.return_temperature, - ATTR_ENABLED_FN: lambda x: True, - }, +ATW_ZONE_SENSORS: dict[str, SensorMetadata] = { + "room_temperature": SensorMetadata( + "Room Temperature", + icon="mdi:thermometer", + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + value_fn=lambda zone: zone.room_temperature, + enabled=lambda x: True, + ), + "flow_temperature": SensorMetadata( + "Flow Temperature", + icon="mdi:thermometer", + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + value_fn=lambda zone: zone.flow_temperature, + enabled=lambda x: True, + ), + "return_temperature": SensorMetadata( + "Flow Return Temperature", + icon="mdi:thermometer", + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + value_fn=lambda zone: zone.return_temperature, + enabled=lambda x: True, + ), } @@ -93,23 +99,23 @@ async def async_setup_entry(hass, entry, async_add_entities): mel_devices = hass.data[DOMAIN].get(entry.entry_id) async_add_entities( [ - MelDeviceSensor(mel_device, measurement, definition) - for measurement, definition in ATA_SENSORS.items() + MelDeviceSensor(mel_device, measurement, metadata) + for measurement, metadata in ATA_SENSORS.items() for mel_device in mel_devices[DEVICE_TYPE_ATA] - if definition[ATTR_ENABLED_FN](mel_device) + if metadata.enabled(mel_device) ] + [ - MelDeviceSensor(mel_device, measurement, definition) - for measurement, definition in ATW_SENSORS.items() + MelDeviceSensor(mel_device, measurement, metadata) + for measurement, metadata in ATW_SENSORS.items() for mel_device in mel_devices[DEVICE_TYPE_ATW] - if definition[ATTR_ENABLED_FN](mel_device) + if metadata.enabled(mel_device) ] + [ - AtwZoneSensor(mel_device, zone, measurement, definition) + AtwZoneSensor(mel_device, zone, measurement, metadata) for mel_device in mel_devices[DEVICE_TYPE_ATW] for zone in mel_device.device.zones - for measurement, definition, in ATW_ZONE_SENSORS.items() - if definition[ATTR_ENABLED_FN](zone) + for measurement, metadata, in ATW_ZONE_SENSORS.items() + if metadata.enabled(zone) ], True, ) @@ -118,25 +124,25 @@ async def async_setup_entry(hass, entry, async_add_entities): class MelDeviceSensor(SensorEntity): """Representation of a Sensor.""" - def __init__(self, api: MelCloudDevice, measurement, definition): + def __init__(self, api: MelCloudDevice, measurement, metadata: SensorMetadata): """Initialize the sensor.""" self._api = api - self._def = definition + self._metadata = metadata - self._attr_device_class = definition[ATTR_DEVICE_CLASS] - self._attr_icon = definition[ATTR_ICON] - self._attr_name = f"{api.name} {definition[ATTR_MEASUREMENT_NAME]}" + self._attr_device_class = metadata.device_class + self._attr_icon = metadata.icon + self._attr_name = f"{api.name} {metadata.measurement_name}" self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{measurement}" - self._attr_unit_of_measurement = definition[ATTR_UNIT] + self._attr_unit_of_measurement = metadata.unit self._attr_state_class = STATE_CLASS_MEASUREMENT - if self.device_class == DEVICE_CLASS_ENERGY: + if metadata.device_class == DEVICE_CLASS_ENERGY: self._attr_last_reset = dt_util.utc_from_timestamp(0) @property def state(self): """Return the state of the sensor.""" - return self._def[ATTR_VALUE_FN](self._api) + return self._metadata.value_fn(self._api) async def async_update(self): """Retrieve latest state.""" @@ -151,17 +157,19 @@ class MelDeviceSensor(SensorEntity): class AtwZoneSensor(MelDeviceSensor): """Air-to-Air device sensor.""" - def __init__(self, api: MelCloudDevice, zone: Zone, measurement, definition): + def __init__( + self, api: MelCloudDevice, zone: Zone, measurement, metadata: SensorMetadata + ): """Initialize the sensor.""" if zone.zone_index == 1: full_measurement = measurement else: full_measurement = f"{measurement}-zone-{zone.zone_index}" - super().__init__(api, full_measurement, definition) + super().__init__(api, full_measurement, metadata) self._zone = zone - self._attr_name = f"{api.name} {zone.name} {definition[ATTR_MEASUREMENT_NAME]}" + self._attr_name = f"{api.name} {zone.name} {metadata.measurement_name}" @property def state(self): """Return zone based state.""" - return self._def[ATTR_VALUE_FN](self._zone) + return self._metadata.value_fn(self._zone) From 8c43e5c7364ac85cfedd221456ef0ea0e01c6eb7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Jul 2021 20:19:26 +0200 Subject: [PATCH 425/818] Correct set_temperature in modbus climate (#52923) --- homeassistant/components/modbus/climate.py | 33 ++++++++++---- tests/components/modbus/test_climate.py | 50 +++++++++++++++++++++- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 75a1f846c76..36871c68d9b 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -33,7 +33,12 @@ from .const import ( CONF_MIN_TEMP, CONF_STEP, CONF_TARGET_TEMP, - DEFAULT_STRUCT_FORMAT, + DATA_TYPE_INT16, + DATA_TYPE_INT32, + DATA_TYPE_INT64, + DATA_TYPE_UINT16, + DATA_TYPE_UINT32, + DATA_TYPE_UINT64, MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -145,16 +150,28 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Set new target temperature.""" if ATTR_TEMPERATURE not in kwargs: return - target_temperature = int( - (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale - ) - byte_string = struct.pack(self._structure, target_temperature) - struct_string = f">{DEFAULT_STRUCT_FORMAT[self._data_type]}" - register_value = struct.unpack(struct_string, byte_string)[0] + target_temperature = ( + float(kwargs.get(ATTR_TEMPERATURE)) - self._offset + ) / self._scale + if self._data_type in [ + DATA_TYPE_INT16, + DATA_TYPE_INT32, + DATA_TYPE_INT64, + DATA_TYPE_UINT16, + DATA_TYPE_UINT32, + DATA_TYPE_UINT64, + ]: + target_temperature = int(target_temperature) + as_bytes = struct.pack(self._structure, target_temperature) + raw_regs = [ + int.from_bytes(as_bytes[i : i + 2], "big") + for i in range(0, len(as_bytes), 2) + ] + registers = self._swap_registers(raw_regs) result = await self._hub.async_pymodbus_call( self._slave, self._target_temperature_register, - register_value, + registers, CALL_TYPE_WRITE_REGISTERS, ) self._available = result is not None diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index b58822644be..b872f4fe302 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -3,7 +3,15 @@ import pytest from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import HVAC_MODE_AUTO -from homeassistant.components.modbus.const import CONF_CLIMATES, CONF_TARGET_TEMP +from homeassistant.components.modbus.const import ( + CONF_CLIMATES, + CONF_DATA_TYPE, + CONF_TARGET_TEMP, + DATA_TYPE_FLOAT32, + DATA_TYPE_FLOAT64, + DATA_TYPE_INT16, + DATA_TYPE_INT32, +) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, @@ -110,6 +118,46 @@ async def test_service_climate_update(hass, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == "auto" +@pytest.mark.parametrize( + "data_type, temperature, result", + [ + (DATA_TYPE_INT16, 35, [0x00]), + (DATA_TYPE_INT32, 36, [0x00, 0x00]), + (DATA_TYPE_FLOAT32, 37.5, [0x00, 0x00]), + (DATA_TYPE_FLOAT64, "39", [0x00, 0x00, 0x00, 0x00]), + ], +) +async def test_service_climate_set_temperature( + hass, data_type, temperature, result, mock_pymodbus +): + """Run test for service homeassistant.update_entity.""" + config = { + CONF_CLIMATES: [ + { + CONF_NAME: CLIMATE_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: data_type, + } + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult(result) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + CLIMATE_DOMAIN, + "set_temperature", + { + "entity_id": ENTITY_ID, + ATTR_TEMPERATURE: temperature, + }, + blocking=True, + ) + + test_value = State(ENTITY_ID, 35) test_value.attributes = {ATTR_TEMPERATURE: 37} From a2fbc4218de269b545e59dde338a8d4afb7a30d9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 20 Jul 2021 13:21:48 -0500 Subject: [PATCH 426/818] Cleanup regroup handling in Sonos (#53241) Check event before creating coroutine Remove unnecessary regrouping dispatcher Update typing to reflect actual behavior Add optimizations for polling mode --- homeassistant/components/sonos/__init__.py | 16 +------- homeassistant/components/sonos/const.py | 1 - homeassistant/components/sonos/speaker.py | 44 +++++++++++----------- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index ec26373c44e..26541db5fd8 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -19,11 +19,7 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOSTS, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send @@ -35,7 +31,6 @@ from .const import ( DISCOVERY_INTERVAL, DOMAIN, PLATFORMS, - SONOS_GROUP_UPDATE, SONOS_REBOOTED, SONOS_SEEN, UPNP_ST, @@ -226,10 +221,6 @@ class SonosDiscoveryManager: DISCOVERY_INTERVAL.total_seconds(), self._manual_hosts ) - @callback - def _async_signal_update_groups(self, _event): - async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE) - def _discovered_ip(self, ip_address): soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED) if soco and soco.is_visible: @@ -290,11 +281,6 @@ class SonosDiscoveryManager: for platform in PLATFORMS ) ) - self.entry.async_on_unload( - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._async_signal_update_groups - ) - ) self.entry.async_on_unload( self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index aca4b9b39ae..88b71066486 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -140,7 +140,6 @@ SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" -SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_ALARMS_UPDATED = "sonos_alarms_updated" SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" SONOS_STATE_UPDATED = "sonos_state_updated" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 7522f72b20b..3ff6627bb8a 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -46,7 +46,6 @@ from .const import ( SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, - SONOS_GROUP_UPDATE, SONOS_POLL_UPDATE, SONOS_REBOOTED, SONOS_SEEN, @@ -206,11 +205,6 @@ class SonosSpeaker: f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity, ) - self._group_dispatcher = dispatcher_connect( - self.hass, - SONOS_GROUP_UPDATE, - self.async_update_groups, - ) self._seen_dispatcher = dispatcher_connect( self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) @@ -612,22 +606,18 @@ class SonosSpeaker: # # Group management # - def update_groups(self, event: SonosEvent | None = None) -> None: - """Handle callback for topology change event.""" - coro = self.create_update_groups_coro(event) - if coro: - self.hass.add_job(coro) # type: ignore + def update_groups(self) -> None: + """Update group topology when polling.""" + self.hass.add_job(self.create_update_groups_coro()) @callback - def async_update_groups(self, event: SonosEvent | None = None) -> None: + def async_update_groups(self, event: SonosEvent) -> None: """Handle callback for topology change event.""" - coro = self.create_update_groups_coro(event) - if coro: - self.hass.async_add_job(coro) # type: ignore + if not hasattr(event, "zone_player_uui_ds_in_group"): + return None + self.hass.async_add_job(self.create_update_groups_coro(event)) - def create_update_groups_coro( - self, event: SonosEvent | None = None - ) -> Coroutine | None: + def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: """Handle callback for topology change event.""" def _get_soco_group() -> list[str]: @@ -646,7 +636,7 @@ class SonosSpeaker: return [coordinator_uid] + slave_uids - async def _async_extract_group(event: SonosEvent) -> list[str]: + async def _async_extract_group(event: SonosEvent | None) -> list[str]: """Extract group layout from a topology event.""" group = event and event.zone_player_uui_ds_in_group if group: @@ -658,6 +648,10 @@ class SonosSpeaker: @callback def _async_regroup(group: list[str]) -> None: """Rebuild internal group layout.""" + if group == [self.soco.uid] and self.sonos_group == [self]: + # Skip updating existing single speakers in polling mode + return + entity_registry = ent_reg.async_get(self.hass) sonos_group = [] sonos_group_entities = [] @@ -671,6 +665,11 @@ class SonosSpeaker: ) sonos_group_entities.append(entity_id) + if self.sonos_group_entities == sonos_group_entities: + # Useful in polling mode for speakers with stereo pairs or surrounds + # as those "invisible" speakers will bypass the single speaker check + return + self.coordinator = None self.sonos_group = sonos_group self.sonos_group_entities = sonos_group_entities @@ -684,7 +683,9 @@ class SonosSpeaker: slave.sonos_group_entities = sonos_group_entities slave.async_write_entity_states() - async def _async_handle_group_event(event: SonosEvent) -> None: + _LOGGER.debug("Regrouped %s: %s", self.zone_name, self.sonos_group_entities) + + async def _async_handle_group_event(event: SonosEvent | None) -> None: """Get async lock and handle event.""" async with self.hass.data[DATA_SONOS].topology_condition: @@ -695,9 +696,6 @@ class SonosSpeaker: self.hass.data[DATA_SONOS].topology_condition.notify_all() - if event and not hasattr(event, "zone_player_uui_ds_in_group"): - return None - return _async_handle_group_event(event) @soco_error() From 5ccbac5ff6ed7a8de330b718bb794467c7d46763 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Tue, 20 Jul 2021 14:23:22 -0400 Subject: [PATCH 427/818] Fix alert infinite loop on repeat interval of 0 (#52628) * #4851 - Infinite loop on repeat interval of 0 Notification will enter an infinite loop when the repeat interval is specified as zero and it is the last repeat configured. When this occurs avoid the infinite loop and log a warning message. Note: I encountered this issue when routing SMS to Twilio and quickly sent thousands of text messages. * Update __init__.py * Remove runtime check since configuration input is now blocked * Tweak comment Co-authored-by: Franck Nijhof --- homeassistant/components/alert/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 69edb3ee001..73be34e6d33 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -48,7 +48,12 @@ ALERT_SCHEMA = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_STATE, default=STATE_ON): cv.string, - vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), + vol.Required(CONF_REPEAT): vol.All( + cv.ensure_list, + [vol.Coerce(float)], + # Minimum delay is 1 second = 0.016 minutes + [vol.Range(min=0.016)], + ), vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Optional(CONF_ALERT_MESSAGE): cv.template, From 034251f0069e5ee670edadf4c5e732607728bbad Mon Sep 17 00:00:00 2001 From: web-dc <49026778+web-dc@users.noreply.github.com> Date: Tue, 20 Jul 2021 20:28:43 +0200 Subject: [PATCH 428/818] Update requirement of homematicip_cloud component to v1.0.1 (#51407) Co-authored-by: Franck Nijhof Co-authored-by: Sascha Schiegg --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/fixtures/homematicip_cloud.json | 9 ++++++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index f82e2c19996..b41c7b06c74 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.13.1"], + "requirements": ["homematicip==1.0.1"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1a79b9d76d4..a7ce60b1bff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,7 +784,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.13.1 +homematicip==1.0.1 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04835c3d369..b9f4b83d37f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.13.1 +homematicip==1.0.1 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 4579fad30ba..8462295cbc1 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -7037,7 +7037,7 @@ "dutyCycle": false, "homeId": "00000000-0000-0000-0000-000000000001", "id": "00000000-0000-0000-0000-000000000016", - "ignorableDevices": [], + "ignorableDeviceChannels": [], "label": "INTERNAL", "lastStatusUpdate": 1524515489257, "lowBat": false, @@ -7373,7 +7373,7 @@ "dutyCycle": false, "homeId": "00000000-0000-0000-0000-000000000001", "id": "00000000-0000-0000-0000-000000000005", - "ignorableDevices": [], + "ignorableDeviceChannels": [], "label": "EXTERNAL", "lastStatusUpdate": 1524516526498, "lowBat": false, @@ -8363,7 +8363,10 @@ "activationInProgress": false, "active": true, "alarmActive": false, - "alarmEventDeviceId": "3014F7110000000000000007", + "alarmEventDeviceChannel": { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, "alarmEventTimestamp": 1524504122047, "alarmSecurityJournalEntryType": "SENSOR_EVENT", "functionalGroups": [ From 059a9bc8ed97bd3067ad912cca2189814c358eba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 20 Jul 2021 22:03:10 +0200 Subject: [PATCH 429/818] Fix modbus setting string as temperature in climate platform (#53249) --- homeassistant/components/modbus/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 36871c68d9b..e8f281bdd1f 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -207,4 +207,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self.unpack_structure_result(result.registers) self._available = True - return self._value + + if self._value is None: + return None + return float(self._value) From a9b9c4f13c25b1213ccc37b8f15dd9ebe39cefa6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 20 Jul 2021 16:26:52 -0400 Subject: [PATCH 430/818] Add extra state attributes to goalzero (#52932) * Add extra state attributes to goalzero * tweak --- homeassistant/components/goalzero/__init__.py | 12 ++++++++++-- homeassistant/components/goalzero/const.py | 9 +++++++++ homeassistant/components/goalzero/sensor.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index f3889d1d385..fe2d5cc695e 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSO from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -17,7 +17,13 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, MIN_TIME_BETWEEN_UPDATES +from .const import ( + ATTRIBUTION, + DATA_KEY_API, + DATA_KEY_COORDINATOR, + DOMAIN, + MIN_TIME_BETWEEN_UPDATES, +) _LOGGER = logging.getLogger(__name__) @@ -74,6 +80,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class YetiEntity(CoordinatorEntity): """Representation of a Goal Zero Yeti entity.""" + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + def __init__(self, api, coordinator, name, server_unique_id): """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index ce4b610b3f0..4c860a5e0e4 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -31,6 +31,7 @@ from homeassistant.const import ( TIME_SECONDS, ) +ATTRIBUTION = "Data provided by Goal Zero" ATTR_DEFAULT_ENABLED = "default_enabled" CONF_IDENTIFIERS = "identifiers" @@ -133,6 +134,14 @@ SENSOR_DICT = { ATTR_UNIT_OF_MEASUREMENT: TIME_SECONDS, ATTR_DEFAULT_ENABLED: False, }, + "ssid": { + ATTR_NAME: "Wi-Fi SSID", + ATTR_DEFAULT_ENABLED: False, + }, + "ipAddr": { + ATTR_NAME: "IP Address", + ATTR_DEFAULT_ENABLED: False, + }, } SWITCH_DICT = { diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 8464104a61f..ccb39ae0813 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -56,4 +56,4 @@ class YetiSensor(YetiEntity): def state(self): """Return the state.""" if self.api.data: - return self.api.data[self._condition] + return self.api.data.get(self._condition) From 0b8b45818df26a872ee947f03f49007e6e8d53e4 Mon Sep 17 00:00:00 2001 From: jtitley Date: Wed, 21 Jul 2021 06:57:47 +1000 Subject: [PATCH 431/818] Update BlinkStick to 1.2.0 (#52244) Co-authored-by: Franck Nijhof --- homeassistant/components/blinksticklight/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index 2520d2b1fcc..05f8fe65fb3 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -2,7 +2,7 @@ "domain": "blinksticklight", "name": "BlinkStick", "documentation": "https://www.home-assistant.io/integrations/blinksticklight", - "requirements": ["blinkstick==1.1.8"], + "requirements": ["blinkstick==1.2.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a7ce60b1bff..76f3f940306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -373,7 +373,7 @@ blebox_uniapi==1.3.3 blinkpy==0.17.0 # homeassistant.components.blinksticklight -blinkstick==1.1.8 +blinkstick==1.2.0 # homeassistant.components.blinkt # blinkt==0.1.0 From 0fd88e7e666ddbd31fbd0aaea15eb0dff35cc129 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 20 Jul 2021 15:41:03 -0600 Subject: [PATCH 432/818] Type _attr_extra_state_attributes as a MutableMapping (#52616) * Type extra_state_attributes as a MutableMapping * Update homeassistant/helpers/entity.py Co-authored-by: Ruslan Sayfutdinov * Update homeassistant/helpers/entity.py Co-authored-by: Ruslan Sayfutdinov --- homeassistant/helpers/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 187d53ea00b..a50afd410e9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC import asyncio -from collections.abc import Awaitable, Iterable, Mapping +from collections.abc import Awaitable, Iterable, Mapping, MutableMapping from datetime import datetime, timedelta import functools as ft import logging @@ -227,7 +227,7 @@ class Entity(ABC): _attr_device_info: DeviceInfo | None = None _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool = True - _attr_extra_state_attributes: Mapping[str, Any] | None = None + _attr_extra_state_attributes: MutableMapping[str, Any] | None = None _attr_force_update: bool = False _attr_icon: str | None = None _attr_name: str | None = None From 6ee82e103188b46d5ec4085e26c015d6f3a43c88 Mon Sep 17 00:00:00 2001 From: Brett Date: Wed, 21 Jul 2021 09:38:50 +1000 Subject: [PATCH 433/818] Advantage Air add zone temperature sensors (#51941) * Create AdvantageAirZoneTemp * Disable by default * Add test coverage * add state_class * Use entity class attributes * Match code style of PR #52498 --- .../components/advantage_air/sensor.py | 25 ++++++++++++++++-- tests/components/advantage_air/test_sensor.py | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index edf079e1cba..e2bf90e73c3 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -2,7 +2,7 @@ import voluptuous as vol from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN @@ -25,9 +25,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) for zone_key, zone in ac_device["zones"].items(): - # Only show damper sensors when zone is in temperature control + # Only show damper and temp sensors when zone is in temperature control if zone["type"] != 0: entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key)) # Only show wireless signal strength sensors when using wireless sensors if zone["rssi"] > 0: entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) @@ -144,3 +145,23 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): if self._zone["rssi"] >= 20: return "mdi:wifi-strength-1" return "mdi:wifi-strength-outline" + + +class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): + """Representation of Advantage Air Zone wireless signal sensor.""" + + _attr_unit_of_measurement = TEMP_CELSIUS + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_icon = "mdi:thermometer" + _attr_entity_registry_enabled_default = False + + def __init__(self, instance, ac_key, zone_key): + """Initialize an Advantage Air Zone Temp Sensor.""" + super().__init__(instance, ac_key, zone_key) + self._attr_name = f'{self._zone["name"]} Temperature' + self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-temp' + + @property + def state(self): + """Return the current value of the measured temperature.""" + return self._zone["measuredTemp"] diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 684b965d94f..997f11dea91 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -1,5 +1,6 @@ """Test the Advantage Air Sensor Platform.""" +from datetime import timedelta from json import loads from homeassistant.components.advantage_air.const import DOMAIN as ADVANTAGE_AIR_DOMAIN @@ -7,9 +8,12 @@ from homeassistant.components.advantage_air.sensor import ( ADVANTAGE_AIR_SERVICE_SET_TIME_TO, ADVANTAGE_AIR_SET_COUNTDOWN_VALUE, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt +from tests.common import async_fire_time_changed from tests.components.advantage_air import ( TEST_SET_RESPONSE, TEST_SET_URL, @@ -125,3 +129,25 @@ async def test_sensor_platform(hass, aioclient_mock): entry = registry.async_get(entity_id) assert entry assert entry.unique_id == "uniqueid-ac1-z02-signal" + + # Test First Zone Temp Sensor (disabled by default) + entity_id = "sensor.zone_open_with_sensor_temperature" + + assert not hass.states.get(entity_id) + + registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert int(state.state) == 25 + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-ac1-z01-temp" From 9d93f8b6d1f19e4ef57249003ed03f734bf6a5d6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 21 Jul 2021 00:11:58 +0000 Subject: [PATCH 434/818] [ci skip] Translation update --- .../components/airvisual/translations/ru.json | 6 ++-- .../airvisual/translations/sensor.ca.json | 20 +++++++++++ .../airvisual/translations/sensor.et.json | 20 +++++++++++ .../airvisual/translations/sensor.nl.json | 20 +++++++++++ .../airvisual/translations/sensor.ru.json | 20 +++++++++++ .../translations/sensor.zh-Hant.json | 20 +++++++++++ .../components/co2signal/translations/ca.json | 34 +++++++++++++++++++ .../components/co2signal/translations/et.json | 34 +++++++++++++++++++ .../components/co2signal/translations/nl.json | 34 +++++++++++++++++++ .../components/co2signal/translations/ru.json | 34 +++++++++++++++++++ .../co2signal/translations/zh-Hant.json | 34 +++++++++++++++++++ .../components/honeywell/translations/ca.json | 17 ++++++++++ .../components/honeywell/translations/et.json | 17 ++++++++++ .../components/honeywell/translations/nl.json | 17 ++++++++++ .../components/honeywell/translations/ru.json | 17 ++++++++++ .../honeywell/translations/zh-Hant.json | 17 ++++++++++ .../switcher_kis/translations/et.json | 13 +++++++ .../switcher_kis/translations/nl.json | 13 +++++++ 18 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.ca.json create mode 100644 homeassistant/components/airvisual/translations/sensor.et.json create mode 100644 homeassistant/components/airvisual/translations/sensor.nl.json create mode 100644 homeassistant/components/airvisual/translations/sensor.ru.json create mode 100644 homeassistant/components/airvisual/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/co2signal/translations/ca.json create mode 100644 homeassistant/components/co2signal/translations/et.json create mode 100644 homeassistant/components/co2signal/translations/nl.json create mode 100644 homeassistant/components/co2signal/translations/ru.json create mode 100644 homeassistant/components/co2signal/translations/zh-Hant.json create mode 100644 homeassistant/components/honeywell/translations/ca.json create mode 100644 homeassistant/components/honeywell/translations/et.json create mode 100644 homeassistant/components/honeywell/translations/nl.json create mode 100644 homeassistant/components/honeywell/translations/ru.json create mode 100644 homeassistant/components/honeywell/translations/zh-Hant.json create mode 100644 homeassistant/components/switcher_kis/translations/et.json create mode 100644 homeassistant/components/switcher_kis/translations/nl.json diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json index 4f0073d3132..f774ec76aaf 100644 --- a/homeassistant/components/airvisual/translations/ru.json +++ b/homeassistant/components/airvisual/translations/ru.json @@ -17,7 +17,7 @@ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" }, - "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.", + "description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043f\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, "geography_by_name": { @@ -25,9 +25,9 @@ "api_key": "\u041a\u043b\u044e\u0447 API", "city": "\u0413\u043e\u0440\u043e\u0434", "country": "\u0421\u0442\u0440\u0430\u043d\u0430", - "state": "\u0448\u0442\u0430\u0442" + "state": "\u0428\u0442\u0430\u0442" }, - "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.", + "description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, "node_pro": { diff --git a/homeassistant/components/airvisual/translations/sensor.ca.json b/homeassistant/components/airvisual/translations/sensor.ca.json new file mode 100644 index 00000000000..236dca64d4e --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.ca.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f2xid de carboni", + "n2": "Di\u00f2xid de nitrogen", + "o3": "Oz\u00f3", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Di\u00f2xid de sofre" + }, + "airvisual__pollutant_level": { + "good": "Bo", + "hazardous": "Perill\u00f3s", + "moderate": "Moderat", + "unhealthy": "Poc saludable", + "unhealthy_sensitive": "Poc saludable per a grups sensibles", + "very_unhealthy": "Molt poc saludable" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.et.json b/homeassistant/components/airvisual/translations/sensor.et.json new file mode 100644 index 00000000000..14f3d82c11d --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.et.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Vingugaas", + "n2": "L\u00e4mmastikdioksiid", + "o3": "Osoon", + "p1": "PM10 osakesed", + "p2": "PM2.5 osakesed", + "s2": "V\u00e4\u00e4veldioksiid" + }, + "airvisual__pollutant_level": { + "good": "Hea", + "hazardous": "Ohtlik", + "moderate": "M\u00f5\u00f5dukas", + "unhealthy": "Ebatervislik", + "unhealthy_sensitive": "Ebatervislik riskir\u00fchmale", + "very_unhealthy": "V\u00e4ga ebatervislik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.nl.json b/homeassistant/components/airvisual/translations/sensor.nl.json new file mode 100644 index 00000000000..72f07853e49 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.nl.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Koolmonoxide", + "n2": "Stikstofdioxide", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Zwaveldioxide" + }, + "airvisual__pollutant_level": { + "good": "Goed", + "hazardous": "Gevaarlijk", + "moderate": "Matig", + "unhealthy": "Ongezond", + "unhealthy_sensitive": "Ongezond voor gevoelige groepen", + "very_unhealthy": "Heel ongezond" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.ru.json b/homeassistant/components/airvisual/translations/sensor.ru.json new file mode 100644 index 00000000000..d75bcc4ee9e --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.ru.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "\u0423\u0433\u0430\u0440\u043d\u044b\u0439 \u0433\u0430\u0437", + "n2": "\u0414\u0438\u043e\u043a\u0441\u0438\u0434 \u0430\u0437\u043e\u0442\u0430", + "o3": "\u041e\u0437\u043e\u043d", + "p1": "PM10", + "p2": "PM2.5", + "s2": "\u0414\u0438\u043e\u043a\u0441\u0438\u0434 \u0441\u0435\u0440\u044b" + }, + "airvisual__pollutant_level": { + "good": "\u0425\u043e\u0440\u043e\u0448\u043e", + "hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e", + "moderate": "\u0421\u0440\u0435\u0434\u043d\u0435", + "unhealthy": "\u0412\u0440\u0435\u0434\u043d\u043e", + "unhealthy_sensitive": "\u0412\u0440\u0435\u0434\u043d\u043e \u0434\u043b\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u044b\u0445 \u0433\u0440\u0443\u043f\u043f", + "very_unhealthy": "\u041e\u0447\u0435\u043d\u044c \u0432\u0440\u0435\u0434\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.zh-Hant.json b/homeassistant/components/airvisual/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..cedd3e33ae6 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.zh-Hant.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "\u4e00\u6c27\u5316\u78b3", + "n2": "\u4e8c\u6c27\u5316\u6c2e", + "o3": "\u81ed\u6c27", + "p1": "PM10", + "p2": "PM2.5", + "s2": "\u4e8c\u6c27\u5316\u786b" + }, + "airvisual__pollutant_level": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u96aa", + "moderate": "\u4e2d\u7b49", + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_sensitive": "\u5c0d\u654f\u611f\u65cf\u7fa4\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u975e\u5e38\u4e0d\u5065\u5eb7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/ca.json b/homeassistant/components/co2signal/translations/ca.json new file mode 100644 index 00000000000..8a9539cfa97 --- /dev/null +++ b/homeassistant/components/co2signal/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "api_ratelimit": "S'ha superat la taxa l\u00edmit d'API", + "unknown": "Error inesperat" + }, + "error": { + "api_ratelimit": "S'ha superat la taxa l\u00edmit d'API", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, + "country": { + "data": { + "country_code": "Codi de pa\u00eds" + } + }, + "user": { + "data": { + "api_key": "Token d'acc\u00e9s", + "location": "Obt\u00e9 dades per" + }, + "description": "Visita https://co2signal.com/ per demanar un token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/et.json b/homeassistant/components/co2signal/translations/et.json new file mode 100644 index 00000000000..a0d8f9db27f --- /dev/null +++ b/homeassistant/components/co2signal/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "api_ratelimit": "API p\u00e4rigute limiit on \u00fcletatud", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "api_ratelimit": "API p\u00e4rigute limiit on \u00fcletatud", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + } + }, + "country": { + "data": { + "country_code": "Riigi kood" + } + }, + "user": { + "data": { + "api_key": "Juurdep\u00e4\u00e4sut\u00f5end", + "location": "Hangi andmed" + }, + "description": "Loa taotlemiseks k\u00fclasta https://co2signal.com/." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/nl.json b/homeassistant/components/co2signal/translations/nl.json new file mode 100644 index 00000000000..54a7cd110cc --- /dev/null +++ b/homeassistant/components/co2signal/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "api_ratelimit": "API Ratelimit overschreden", + "unknown": "Onverwachte fout" + }, + "error": { + "api_ratelimit": "API Ratelimit overschreden", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + } + }, + "country": { + "data": { + "country_code": "Landcode" + } + }, + "user": { + "data": { + "api_key": "Toegangstoken", + "location": "Gegevens ophalen voor" + }, + "description": "Ga naar https://co2signal.com/ om een token aan te vragen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/ru.json b/homeassistant/components/co2signal/translations/ru.json new file mode 100644 index 00000000000..c2be73b3c26 --- /dev/null +++ b/homeassistant/components/co2signal/translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "api_ratelimit": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d \u043f\u0440\u0435\u0434\u0435\u043b \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 API.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "api_ratelimit": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d \u043f\u0440\u0435\u0434\u0435\u043b \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 API.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + } + }, + "country": { + "data": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b" + } + }, + "user": { + "data": { + "api_key": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "location": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f" + }, + "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://co2signal.com/." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/zh-Hant.json b/homeassistant/components/co2signal/translations/zh-Hant.json new file mode 100644 index 00000000000..39cee0da0e5 --- /dev/null +++ b/homeassistant/components/co2signal/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "api_ratelimit": "\u8d85\u904e API \u5b58\u53d6\u9650\u5236", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "api_ratelimit": "\u8d85\u904e API \u5b58\u53d6\u9650\u5236", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + } + }, + "country": { + "data": { + "country_code": "\u570b\u78bc" + } + }, + "user": { + "data": { + "api_key": "\u5b58\u53d6\u6b0a\u6756", + "location": "\u53d6\u5f97\u8cc7\u6599\uff1a" + }, + "description": "\u700f\u89bd https://co2signal.com/ \u4ee5\u7372\u5f97\u6b0a\u6756\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/ca.json b/homeassistant/components/honeywell/translations/ca.json new file mode 100644 index 00000000000..34da1b89f10 --- /dev/null +++ b/homeassistant/components/honeywell/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les credencials utilitzades per iniciar sessi\u00f3 a mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (EUA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/et.json b/homeassistant/components/honeywell/translations/et.json new file mode 100644 index 00000000000..264a1efeca5 --- /dev/null +++ b/homeassistant/components/honeywell/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta saidile mytotalconnectcomfort.com sisenemiseks kasutatav mandaat.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/nl.json b/homeassistant/components/honeywell/translations/nl.json new file mode 100644 index 00000000000..0abd80fa088 --- /dev/null +++ b/homeassistant/components/honeywell/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer de inloggegevens in die zijn gebruikt om in te loggen op mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (US)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/ru.json b/homeassistant/components/honeywell/translations/ru.json new file mode 100644 index 00000000000..1d775e6c2c7 --- /dev/null +++ b/homeassistant/components/honeywell/translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0430 mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (\u0421\u0428\u0410)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/zh-Hant.json b/homeassistant/components/honeywell/translations/zh-Hant.json new file mode 100644 index 00000000000..906506d41a5 --- /dev/null +++ b/homeassistant/components/honeywell/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165\u767b\u5165 mytotalconnectcomfort.com \u4e4b\u6191\u8b49\u3002", + "title": "Honeywell Total Connect Comfort\uff08\u7f8e\u570b\uff09" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/et.json b/homeassistant/components/switcher_kis/translations/et.json new file mode 100644 index 00000000000..9e7bb472e0d --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/nl.json b/homeassistant/components/switcher_kis/translations/nl.json new file mode 100644 index 00000000000..d11896014fd --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + } + } + } +} \ No newline at end of file From 72bc7480818b733e633f0d234bc62b6a9dde1a63 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 21 Jul 2021 03:46:33 +0200 Subject: [PATCH 435/818] Avoid supplemental discovery of ignored upnp entry (#53250) --- homeassistant/components/upnp/config_flow.py | 6 ++- homeassistant/components/upnp/device.py | 4 ++ tests/components/upnp/test_config_flow.py | 56 ++++++++------------ 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index f52ce89660d..0679d9ffcb5 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -166,8 +166,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery = discovery_info_to_discovery(discovery_info) # Ensure not already configuring/configured. - discovery = await Device.async_supplement_discovery(self.hass, discovery) - unique_id = discovery[DISCOVERY_UNIQUE_ID] + unique_id = discovery[DISCOVERY_USN] await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured( updates={CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME]} @@ -183,6 +182,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="discovery_ignored") + # Get more data about the device. + discovery = await Device.async_supplement_discovery(self.hass, discovery) + # Store discovery. self._discoveries = [discovery] diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 9af7cf55c24..cf76aa41f8a 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -40,11 +40,15 @@ from .const import ( def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping: """Convert a SSDP-discovery to 'our' discovery.""" + location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + parsed = urlparse(location) + hostname = parsed.hostname return { DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN], DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST], DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION], DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN], + DISCOVERY_HOSTNAME: hostname, } diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 6a911f8d4db..6e546be93f3 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch +from urllib.parse import urlparse from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp @@ -33,7 +34,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_flow_ssdp_discovery(hass: HomeAssistant): """Test config flow: discovered + configured through ssdp.""" udn = "uuid:device_1" - location = "dummy" + location = "http://dummy" mock_device = MockDevice(udn) ssdp_discoveries = [ { @@ -93,7 +94,7 @@ async def test_flow_ssdp_discovery(hass: HomeAssistant): async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): """Test config flow: incomplete discovery through ssdp.""" udn = "uuid:device_1" - location = "dummy" + location = "http://dummy" mock_device = MockDevice(udn) # Discovered via step ssdp. @@ -112,9 +113,9 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): - """Test config flow: discovery through ssdp, but ignored.""" + """Test config flow: discovery through ssdp, but ignored, as hostname is used by existing config entry.""" udn = "uuid:device_random_1" - location = "dummy" + location = "http://dummy" mock_device = MockDevice(udn) # Existing entry. @@ -123,46 +124,31 @@ async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): data={ CONFIG_ENTRY_UDN: "uuid:device_random_2", CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, + CONFIG_ENTRY_HOSTNAME: urlparse(location).hostname, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) config_entry.add_to_hass(hass) - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] - - with patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step ssdp, but ignored. - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "discovery_ignored" + # Discovered via step ssdp, but ignored. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_SSDP_USN: mock_device.usn, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "discovery_ignored" async def test_flow_user(hass: HomeAssistant): """Test config flow: discovered + configured through user.""" udn = "uuid:device_1" - location = "dummy" + location = "http://dummy" mock_device = MockDevice(udn) ssdp_discoveries = [ { @@ -217,7 +203,7 @@ async def test_flow_import(hass: HomeAssistant): """Test config flow: discovered + configured through configuration.yaml.""" udn = "uuid:device_1" mock_device = MockDevice(udn) - location = "dummy" + location = "http://dummy" ssdp_discoveries = [ { ssdp.ATTR_SSDP_LOCATION: location, From 9d3bc0632f551e19c94cdc68f1f6503ca2e7e067 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 20 Jul 2021 19:47:37 -0600 Subject: [PATCH 436/818] Bump pylitterbot to 2021.7.2 (#53254) * Bump pylitterbot to 2021.7.1 * Bump pylitterbot dependency to 2021.7.2 which unpins Authlib and httpx dependencies --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 346bb5e0761..a22499fb062 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.3.1"], + "requirements": ["pylitterbot==2021.7.2"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 76f3f940306..fb87a9d615d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1548,7 +1548,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.3.1 +pylitterbot==2021.7.2 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9f4b83d37f..42197c6f659 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.3.1 +pylitterbot==2021.7.2 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 56efee4603534855beb4495451506e4ddc53c069 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 20 Jul 2021 20:52:05 -0600 Subject: [PATCH 437/818] Ensure Ambient PWS is strictly typed (#53251) * Ensure Ambient PWS is strictly typed * Fix typing --- .strict-typing | 1 + homeassistant/components/ambient_station/__init__.py | 6 +++--- mypy.ini | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 981e3872eb5..40fb375ca16 100644 --- a/.strict-typing +++ b/.strict-typing @@ -13,6 +13,7 @@ homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.amazon_polly.* homeassistant.components.ambee.* +homeassistant.components.ambient_station.* homeassistant.components.ampio.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index d2f09e47f7b..12f534eb8e9 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -32,7 +32,7 @@ from homeassistant.const import ( SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -320,7 +320,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - async def _async_disconnect_websocket(*_): + async def _async_disconnect_websocket(_: Event) -> None: await ambient.client.websocket.disconnect() config_entry.async_on_unload( @@ -378,7 +378,7 @@ class AmbientStation: async def _attempt_connect(self) -> None: """Attempt to connect to the socket (retrying later on fail).""" - async def connect(timestamp: int | None = None): + async def connect(timestamp: int | None = None) -> None: """Connect.""" await self.client.websocket.connect() diff --git a/mypy.ini b/mypy.ini index 942ef115135..b0d11d55262 100644 --- a/mypy.ini +++ b/mypy.ini @@ -154,6 +154,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ambient_station.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ampio.*] check_untyped_defs = true disallow_incomplete_defs = true From 8f61efe71485ad84bbebca10dc39ff84a6115b27 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 21 Jul 2021 04:53:56 +0200 Subject: [PATCH 438/818] Correct typing in edl21 and activate mypy. (#53188) --- homeassistant/components/edl21/sensor.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 16502632f4f..65ca1ee9050 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -126,7 +126,7 @@ class EDL21: def __init__(self, hass, config, async_add_entities) -> None: """Initialize an EDL21 object.""" - self._registered_obis = set() + self._registered_obis: set[()] = set() self._hass = hass self._async_add_entities = async_add_entities self._name = config[CONF_NAME] diff --git a/mypy.ini b/mypy.ini index b0d11d55262..cf85d2e60a9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1150,9 +1150,6 @@ ignore_errors = true [mypy-homeassistant.components.doorbird.*] ignore_errors = true -[mypy-homeassistant.components.edl21.*] -ignore_errors = true - [mypy-homeassistant.components.elkm1.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a571047446c..7c97134397b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -38,7 +38,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.dhcp.*", "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", - "homeassistant.components.edl21.*", "homeassistant.components.elkm1.*", "homeassistant.components.emonitor.*", "homeassistant.components.enphase_envoy.*", From 4d122fc3669174a646751a612a5056ac59fa5a7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:21:05 -1000 Subject: [PATCH 439/818] Update alexa lock to support locking, unlocking, jammed (#52841) --- homeassistant/components/alexa/capabilities.py | 7 +++++-- tests/components/alexa/test_capabilities.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 10b382c8dcf..db1fa990c54 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -19,6 +19,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_NIGHT, ) import homeassistant.components.climate.const as climate +from homeassistant.components.lock import STATE_LOCKING, STATE_UNLOCKING import homeassistant.components.media_player.const as media_player from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, @@ -446,9 +447,11 @@ class AlexaLockController(AlexaCapability): if name != "lockState": raise UnsupportedProperty(name) - if self.entity.state == STATE_LOCKED: + # If its unlocking its still locked and not unlocked yet + if self.entity.state in (STATE_UNLOCKING, STATE_LOCKED): return "LOCKED" - if self.entity.state == STATE_UNLOCKED: + # If its locking its still unlocked and not locked yet + if self.entity.state in (STATE_LOCKING, STATE_UNLOCKED): return "UNLOCKED" return "JAMMED" diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 92951d4a0e7..dc93ed6d805 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components.alexa import smart_home from homeassistant.components.alexa.errors import UnsupportedProperty from homeassistant.components.climate import const as climate +from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -227,17 +228,29 @@ async def test_report_lock_state(hass): """Test LockController implements lockState property.""" hass.states.async_set("lock.locked", STATE_LOCKED, {}) hass.states.async_set("lock.unlocked", STATE_UNLOCKED, {}) + hass.states.async_set("lock.unlocking", STATE_UNLOCKING, {}) + hass.states.async_set("lock.locking", STATE_LOCKING, {}) + hass.states.async_set("lock.jammed", STATE_JAMMED, {}) hass.states.async_set("lock.unknown", STATE_UNKNOWN, {}) properties = await reported_properties(hass, "lock.locked") properties.assert_equal("Alexa.LockController", "lockState", "LOCKED") + properties = await reported_properties(hass, "lock.unlocking") + properties.assert_equal("Alexa.LockController", "lockState", "LOCKED") + properties = await reported_properties(hass, "lock.unlocked") properties.assert_equal("Alexa.LockController", "lockState", "UNLOCKED") + properties = await reported_properties(hass, "lock.locking") + properties.assert_equal("Alexa.LockController", "lockState", "UNLOCKED") + properties = await reported_properties(hass, "lock.unknown") properties.assert_equal("Alexa.LockController", "lockState", "JAMMED") + properties = await reported_properties(hass, "lock.jammed") + properties.assert_equal("Alexa.LockController", "lockState", "JAMMED") + @pytest.mark.parametrize( "supported_color_modes", [["brightness"], ["hs"], ["color_temp"]] From 2a65c5f93cbd8f448953657ba9196df0652f3c3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:45:21 -1000 Subject: [PATCH 440/818] Recreate HomeKit accessories when calling the reset_accessory service (#53199) --- homeassistant/components/homekit/__init__.py | 119 +++++--- tests/components/homekit/test_homekit.py | 270 ++++++++++++++++++- 2 files changed, 345 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5abc9adb9ca..5d9f2037610 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -120,6 +120,10 @@ PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 MDNS_TARGET_IP = "224.0.0.251" +_HOMEKIT_CONFIG_UPDATE_TIME = ( + 5 # number of seconds to wait for homekit to see the c# change +) + def _has_all_unique_names_and_ports(bridges): """Validate that each homekit bridge configured has a unique name.""" @@ -351,7 +355,7 @@ def _async_register_events_and_services(hass: HomeAssistant): """Register events and services for HomeKit.""" hass.http.register_view(HomeKitPairingQRView) - def handle_homekit_reset_accessory(service): + async def async_handle_homekit_reset_accessory(service): """Handle start HomeKit service call.""" for entry_id in hass.data[DOMAIN]: if HOMEKIT not in hass.data[DOMAIN][entry_id]: @@ -365,12 +369,12 @@ def _async_register_events_and_services(hass: HomeAssistant): continue entity_ids = service.data.get("entity_id") - homekit.reset_accessories(entity_ids) + await homekit.async_reset_accessories(entity_ids) hass.services.async_register( DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY, - handle_homekit_reset_accessory, + async_handle_homekit_reset_accessory, schema=RESET_ACCESSORY_SERVICE_SCHEMA, ) @@ -486,36 +490,61 @@ class HomeKit: self.driver.persist() - def reset_accessories(self, entity_ids): + async def async_reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" if not self.bridge: - self.driver.config_changed() + await self.async_reset_accessories_in_accessory_mode(entity_ids) return + await self.async_reset_accessories_in_bridge_mode(entity_ids) - removed = [] + async def async_reset_accessories_in_accessory_mode(self, entity_ids): + """Reset accessories in accessory mode.""" + acc = self.driver.accessory + if acc.entity_id not in entity_ids: + return + acc.async_stop() + if not (state := self.hass.states.get(acc.entity_id)): + _LOGGER.warning( + "The underlying entity %s disappeared during reset", acc.entity + ) + return + if new_acc := self._async_create_single_accessory([state]): + self.driver.accessory = new_acc + await self.async_config_changed() + + async def async_reset_accessories_in_bridge_mode(self, entity_ids): + """Reset accessories in bridge mode.""" + new = [] for entity_id in entity_ids: aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) if aid not in self.bridge.accessories: continue - _LOGGER.info( "HomeKit Bridge %s will reset accessory with linked entity_id %s", self._name, entity_id, ) - acc = self.remove_bridge_accessory(aid) - removed.append(acc) + if state := self.hass.states.get(acc.entity_id): + new.append(state) + else: + _LOGGER.warning( + "The underlying entity %s disappeared during reset", acc.entity + ) - if not removed: + if not new: # No matched accessories, probably on another bridge return - self.driver.config_changed() + await self.async_config_changed() + await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME) + for state in new: + self.add_bridge_accessory(state) + await self.async_config_changed() - for acc in removed: - self.bridge.add_accessory(acc) - self.driver.config_changed() + async def async_config_changed(self): + """Call config changed which writes out the new config to disk.""" + await self.hass.async_add_executor_job(self.driver.config_changed) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -541,7 +570,7 @@ class HomeKit: ) aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id) - conf = self._config.pop(state.entity_id, {}) + conf = self._config.get(state.entity_id, {}).copy() # If an accessory cannot be created or added due to an exception # of any kind (usually in pyhap) it should not prevent # the rest of the accessories from being created @@ -556,9 +585,9 @@ class HomeKit: def remove_bridge_accessory(self, aid): """Try adding accessory to bridge if configured beforehand.""" - acc = None - if aid in self.bridge.accessories: - acc = self.bridge.accessories.pop(aid) + acc = self.bridge.accessories.pop(aid, None) + if acc: + acc.async_stop() return acc async def async_configure_accessories(self): @@ -665,33 +694,45 @@ class HomeKit: for device_id in devices_to_purge: dev_reg.async_remove_device(device_id) + @callback + def _async_create_single_accessory(self, entity_states): + """Create a single HomeKit accessory (accessory mode).""" + if not entity_states: + _LOGGER.error( + "HomeKit %s cannot startup: entity not available: %s", + self._name, + self._filter.config, + ) + return None + state = entity_states[0] + conf = self._config.get(state.entity_id, {}).copy() + acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) + if acc is None: + _LOGGER.error( + "HomeKit %s cannot startup: entity not supported: %s", + self._name, + self._filter.config, + ) + return acc + + @callback + def _async_create_bridge_accessory(self, entity_states): + """Create a HomeKit bridge with accessories. (bridge mode).""" + self.bridge = HomeBridge(self.hass, self.driver, self._name) + for state in entity_states: + self.add_bridge_accessory(state) + return self.bridge + async def _async_create_accessories(self): """Create the accessories.""" entity_states = await self.async_configure_accessories() if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: - if not entity_states: - _LOGGER.error( - "HomeKit %s cannot startup: entity not available: %s", - self._name, - self._filter.config, - ) - return False - state = entity_states[0] - conf = self._config.pop(state.entity_id, {}) - acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) - if acc is None: - _LOGGER.error( - "HomeKit %s cannot startup: entity not supported: %s", - self._name, - self._filter.config, - ) - return False + acc = self._async_create_single_accessory(entity_states) else: - self.bridge = HomeBridge(self.hass, self.driver, self._name) - for state in entity_states: - self.add_bridge_accessory(state) - acc = self.bridge + acc = self._async_create_bridge_accessory(entity_states) + if acc is None: + return False # No need to load/persist as we do it in setup self.driver.accessory = acc return True diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 235c3027c98..ba34830f381 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -434,10 +434,12 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf): homekit.driver = "driver" homekit.bridge = _mock_pyhap_bridge() - homekit.bridge.accessories = {"light.demo": "acc"} + acc_mock = MagicMock() + homekit.bridge.accessories = {6: acc_mock} - acc = homekit.remove_bridge_accessory("light.demo") - assert acc == "acc" + acc = homekit.remove_bridge_accessory(6) + assert acc is acc_mock + assert acc_mock.async_stop.called assert len(homekit.bridge.accessories) == 0 @@ -627,12 +629,13 @@ async def test_homekit_stop(hass): async def test_homekit_reset_accessories(hass, mock_zeroconf): - """Test adding too many accessories to HomeKit.""" + """Test resetting HomeKit accessories.""" await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entity_id = "light.demo" + hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( @@ -641,11 +644,15 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): "pyhap.accessory_driver.AccessoryDriver.config_changed" ) as hk_driver_config_changed, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" + ), patch.object( + homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 ): await async_init_entry(hass, entry) + acc_mock = MagicMock() + acc_mock.entity_id = entity_id aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) - homekit.bridge.accessories = {aid: "acc"} + homekit.bridge.accessories = {aid: acc_mock} homekit.status = STATUS_RUNNING await hass.services.async_call( @@ -661,6 +668,259 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): homekit.status = STATUS_READY +async def test_homekit_reset_accessories_not_supported(hass, mock_zeroconf): + """Test resetting HomeKit accessories with an unsupported entity.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "not_supported.demo" + hass.states.async_set("not_supported.demo", "on") + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory.Bridge.add_accessory" + ) as mock_add_accessory, patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ), patch.object( + homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 + ): + await async_init_entry(hass, entry) + + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + homekit.bridge.accessories = {aid: acc_mock} + homekit.status = STATUS_RUNNING + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 2 + assert not mock_add_accessory.called + assert len(homekit.bridge.accessories) == 0 + homekit.status = STATUS_STOPPED + + +async def test_homekit_reset_accessories_state_missing(hass, mock_zeroconf): + """Test resetting HomeKit accessories when the state goes missing.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory.Bridge.add_accessory" + ) as mock_add_accessory, patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ), patch.object( + homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 + ): + await async_init_entry(hass, entry) + + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + homekit.bridge.accessories = {aid: acc_mock} + homekit.status = STATUS_RUNNING + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 0 + assert not mock_add_accessory.called + homekit.status = STATUS_STOPPED + + +async def test_homekit_reset_accessories_not_bridged(hass, mock_zeroconf): + """Test resetting HomeKit accessories when the state is not bridged.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory.Bridge.add_accessory" + ) as mock_add_accessory, patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ), patch.object( + homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 + ): + await async_init_entry(hass, entry) + + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + homekit.bridge.accessories = {aid: acc_mock} + homekit.status = STATUS_RUNNING + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: "light.not_bridged"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 0 + assert not mock_add_accessory.called + homekit.status = STATUS_STOPPED + + +async def test_homekit_reset_single_accessory(hass, mock_zeroconf): + """Test resetting HomeKit single accessory.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + hass.states.async_set("light.demo", "on") + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + homekit.status = STATUS_RUNNING + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + homekit.driver.accessory = acc_mock + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 1 + homekit.status = STATUS_READY + + +async def test_homekit_reset_single_accessory_unsupported(hass, mock_zeroconf): + """Test resetting HomeKit single accessory with an unsupported entity.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "not_supported.demo" + hass.states.async_set("not_supported.demo", "on") + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + homekit.status = STATUS_RUNNING + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + homekit.driver.accessory = acc_mock + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 0 + homekit.status = STATUS_STOPPED + + +async def test_homekit_reset_single_accessory_state_missing(hass, mock_zeroconf): + """Test resetting HomeKit single accessory when the state goes missing.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + homekit.status = STATUS_RUNNING + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + homekit.driver.accessory = acc_mock + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 0 + homekit.status = STATUS_STOPPED + + +async def test_homekit_reset_single_accessory_no_match(hass, mock_zeroconf): + """Test resetting HomeKit single accessory when the entity id does not match.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + homekit.status = STATUS_RUNNING + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + homekit.driver.accessory = acc_mock + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: "light.no_match"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hk_driver_config_changed.call_count == 0 + homekit.status = STATUS_STOPPED + + async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroconf): """Test adding too many accessories to HomeKit.""" entry = await async_init_integration(hass) From f20602e11d55b06cbb22bceebe895b13743583ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:46:39 -1000 Subject: [PATCH 441/818] Auto recreate HomeKit TVs when the sources are out of sync (#53208) --- .../components/homekit/accessories.py | 13 ++++++++++ .../components/homekit/type_remotes.py | 24 +++++++++++++++---- .../homekit/test_type_media_players.py | 2 +- tests/components/homekit/test_type_remote.py | 18 ++++++++++++++ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3c843955222..03d00c42a91 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -58,12 +58,14 @@ from .const import ( CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, DEVICE_CLASS_PM25, + DOMAIN, EVENT_HOMEKIT_CHANGED, HK_CHARGING, HK_NOT_CHARGABLE, HK_NOT_CHARGING, MANUFACTURER, SERV_BATTERY_SERVICE, + SERVICE_HOMEKIT_RESET_ACCESSORY, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -454,6 +456,17 @@ class HomeAccessory(Accessory): ) ) + @ha_callback + def async_reset(self): + """Reset and recreate an accessory.""" + self.hass.async_create_task( + self.hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: self.entity_id}, + ) + ) + @ha_callback def async_stop(self): """Cancel any subscriptions when the bridge is stopped.""" diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index e4f18a7c16f..718671dfd1d 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -87,6 +87,7 @@ class RemoteInputSelectAccessory(HomeAccessory): features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) self.source_key = source_key + self.source_list_key = source_list_key self.sources = [] self.support_select_source = False if features & required_feature: @@ -152,13 +153,26 @@ class RemoteInputSelectAccessory(HomeAccessory): index = self.sources.index(source_name) if self.char_input_source.value != index: self.char_input_source.set_value(index) - elif hk_state: - _LOGGER.warning( - "%s: Sources out of sync. Restart Home Assistant", + return + + possible_sources = new_state.attributes.get(self.source_list_key, []) + if source_name in possible_sources: + _LOGGER.debug( + "%s: Sources out of sync. Rebuilding Accessory", self.entity_id, ) - if self.char_input_source.value != 0: - self.char_input_source.set_value(0) + # Sources are out of sync, recreate the accessory + self.async_reset() + return + + _LOGGER.debug( + "%s: Source %s does not exist the source list: %s", + self.entity_id, + source_name, + possible_sources, + ) + if self.char_input_source.value != 0: + self.char_input_source.set_value(0) @TYPES.register("ActivityRemote") diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index b95903d3e3f..33cac7bcf8a 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -242,7 +242,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog): hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: "HDMI 5"}) await hass.async_block_till_done() assert acc.char_input_source.value == 0 - assert caplog.records[-2].levelname == "WARNING" + assert caplog.records[-2].levelname == "DEBUG" # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index e69ebfb29fb..ee71d7f4e3c 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -3,8 +3,10 @@ from homeassistant.components.homekit.const import ( ATTR_KEY_NAME, ATTR_VALUE, + DOMAIN as HOMEKIT_DOMAIN, EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, KEY_ARROW_RIGHT, + SERVICE_HOMEKIT_RESET_ACCESSORY, ) from homeassistant.components.homekit.type_remotes import ActivityRemote from homeassistant.components.remote import ( @@ -146,3 +148,19 @@ async def test_activity_remote(hass, hk_driver, events, caplog): assert len(events) == 1 assert events[0].data[ATTR_KEY_NAME] == KEY_ARROW_RIGHT + + call_reset_accessory = async_mock_service( + hass, HOMEKIT_DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY + ) + # A wild source appears - The accessory should rebuild itself + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "Amazon TV", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "Amazon TV"], + }, + ) + await hass.async_block_till_done() + assert call_reset_accessory[0].data[ATTR_ENTITY_ID] == entity_id From 0ce071e0a4c5fd2c09604038e55d708f51e83d8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:47:13 -1000 Subject: [PATCH 442/818] Bump httpx to 0.18.2 (#53257) --- homeassistant/package_constraints.txt | 6 +----- requirements.txt | 2 +- script/gen_requirements_all.py | 4 ---- setup.py | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f2448e7025b..62b7c5e95d5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 home-assistant-frontend==20210707.0 -httpx==0.18.0 +httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 paho-mqtt==1.5.1 @@ -62,7 +62,3 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 -# httpcore 0.13.4 breaks several integrations -# https://github.com/home-assistant/core/issues/51778 -httpcore==0.13.3 - diff --git a/requirements.txt b/requirements.txt index ad9c2717e94..dd445b8a7e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 -httpx==0.18.0 +httpx==0.18.2 jinja2==3.0.1 PyJWT==1.7.1 cryptography==3.3.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dc0c5fa2c10..4fd96cb1b04 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -83,10 +83,6 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 -# httpcore 0.13.4 breaks several integrations -# https://github.com/home-assistant/core/issues/51778 -httpcore==0.13.3 - """ IGNORE_PRE_COMMIT_HOOK_ID = ( diff --git a/setup.py b/setup.py index 758f4f3813d..db4e8a54d72 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ REQUIRES = [ "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", - "httpx==0.18.0", + "httpx==0.18.2", "jinja2==3.0.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. From bfe3ef09800df2cbca8e4f3a249f983879b60808 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:48:15 -1000 Subject: [PATCH 443/818] Update august to support locking, unlocking, jammed (#52814) --- homeassistant/components/august/lock.py | 22 ++- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/test_lock.py | 129 ++++++++++++++++-- .../fixtures/august/get_activity.jammed.json | 34 +++++ .../fixtures/august/get_activity.locking.json | 34 +++++ .../august/get_activity.unlocking.json | 34 +++++ 8 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/august/get_activity.jammed.json create mode 100644 tests/fixtures/august/get_activity.locking.json create mode 100644 tests/fixtures/august/get_activity.unlocking.json diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e74eded3557..5f4fe85bc71 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,6 +1,7 @@ """Support for August lock.""" import logging +from aiohttp import ClientResponseError from yalexs.activity import SOURCE_PUBNUB, ActivityType from yalexs.lock import LockStatus from yalexs.util import update_lock_detail_from_activity @@ -9,12 +10,15 @@ from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util from .const import DATA_AUGUST, DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) +LOCK_JAMMED_ERR = 531 + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up August locks.""" @@ -44,9 +48,17 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): await self._call_lock_operation(self._data.async_unlock) async def _call_lock_operation(self, lock_operation): - activities = await lock_operation(self._device_id) - for lock_activity in activities: - update_lock_detail_from_activity(self._detail, lock_activity) + try: + activities = await lock_operation(self._device_id) + except ClientResponseError as err: + if err.status == LOCK_JAMMED_ERR: + self._detail.lock_status = LockStatus.JAMMED + self._detail.lock_status_datetime = dt_util.utcnow() + else: + raise + else: + for lock_activity in activities: + update_lock_detail_from_activity(self._detail, lock_activity) if self._update_lock_status_from_detail(): _LOGGER.debug( @@ -91,6 +103,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): else: self._attr_is_locked = self._lock_status is LockStatus.LOCKED + self._attr_is_jammed = self._lock_status is LockStatus.JAMMED + self._attr_is_locking = self._lock_status is LockStatus.LOCKING + self._attr_is_unlocking = self._lock_status is LockStatus.UNLOCKING + self._attr_extra_state_attributes = { ATTR_BATTERY_LEVEL: self._detail.battery_level } diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e966338f287..9f8b435b714 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.11"], + "requirements": ["yalexs==1.1.12"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index fb87a9d615d..a0b2fa1d554 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2401,7 +2401,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.3 # homeassistant.components.august -yalexs==1.1.11 +yalexs==1.1.12 # homeassistant.components.yeelight yeelight==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42197c6f659..36cfed2598e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ xknx==0.18.8 xmltodict==0.12.0 # homeassistant.components.august -yalexs==1.1.11 +yalexs==1.1.12 # homeassistant.components.yeelight yeelight==0.6.3 diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 5b3c163780f..a0b44d4fb79 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -2,9 +2,16 @@ import datetime from unittest.mock import Mock +from aiohttp import ClientResponseError +import pytest from yalexs.pubnub_async import AugustPubNub -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + STATE_JAMMED, + STATE_LOCKING, + STATE_UNLOCKING, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_LOCK, @@ -59,6 +66,44 @@ async def test_lock_changed_by(hass): ) +async def test_state_locking(hass): + """Test creation of a lock with doorsense and bridge that is locking.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture(hass, "get_activity.locking.json") + await _create_august_with_devices(hass, [lock_one], activities=activities) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKING + + +async def test_state_unlocking(hass): + """Test creation of a lock with doorsense and bridge that is unlocking.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture( + hass, "get_activity.unlocking.json" + ) + await _create_august_with_devices(hass, [lock_one], activities=activities) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + + +async def test_state_jammed(hass): + """Test creation of a lock with doorsense and bridge that is jammed.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture(hass, "get_activity.jammed.json") + await _create_august_with_devices(hass, [lock_one], activities=activities) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_JAMMED + + async def test_one_lock_operation(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) @@ -109,6 +154,74 @@ async def test_one_lock_operation(hass): ) +async def test_lock_jammed(hass): + """Test lock gets jammed on unlock.""" + + def _unlock_return_activities_side_effect(access_token, device_id): + raise ClientResponseError(None, None, status=531) + + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + await _create_august_with_devices( + hass, + [lock_one], + api_call_side_effects={ + "unlock_return_activities": _unlock_return_activities_side_effect + }, + ) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 + assert ( + lock_online_with_doorsense_name.attributes.get("friendly_name") + == "online_with_doorsense Name" + ) + + data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True + ) + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_JAMMED + + +async def test_lock_throws_exception_on_unknown_status_code(hass): + """Test lock throws exception.""" + + def _unlock_return_activities_side_effect(access_token, device_id): + raise ClientResponseError(None, None, status=500) + + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + await _create_august_with_devices( + hass, + [lock_one], + api_call_side_effects={ + "unlock_return_activities": _unlock_return_activities_side_effect + }, + ) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 + assert ( + lock_online_with_doorsense_name.attributes.get("friendly_name") + == "online_with_doorsense Name" + ) + + data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} + with pytest.raises(ClientResponseError): + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True + ) + await hass.async_block_till_done() + + async def test_one_lock_unknown_state(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_lock_from_fixture( @@ -178,7 +291,7 @@ async def test_lock_update_via_pubnub(hass): await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + assert lock_online_with_doorsense_name.state == STATE_UNLOCKING pubnub.message( pubnub, @@ -193,24 +306,24 @@ async def test_lock_update_via_pubnub(hass): await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING pubnub.connected = True async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING # Ensure pubnub status is always preserved async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING pubnub.message( pubnub, @@ -224,12 +337,12 @@ async def test_lock_update_via_pubnub(hass): ) await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + assert lock_online_with_doorsense_name.state == STATE_UNLOCKING async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + assert lock_online_with_doorsense_name.state == STATE_UNLOCKING await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/fixtures/august/get_activity.jammed.json b/tests/fixtures/august/get_activity.jammed.json new file mode 100644 index 00000000000..be5b9dfa4eb --- /dev/null +++ b/tests/fixtures/august/get_activity.jammed.json @@ -0,0 +1,34 @@ +[{ + "entities" : { + "activity" : "mockActivity2", + "house" : "123", + "device" : "online_with_doorsense", + "callingUser" : "mockUserId2", + "otherUser" : "deleted" + }, + "callingUser" : { + "LastName" : "elven princess", + "UserID" : "mockUserId2", + "FirstName" : "Your favorite" + }, + "otherUser" : { + "LastName" : "User", + "UserName" : "deleteduser", + "FirstName" : "Unknown", + "UserID" : "deleted", + "PhoneNo" : "deleted" + }, + "deviceType" : "lock", + "deviceName" : "MockHouseTDoor", + "action" : "jammed", + "dateTime" : 1582007218000, + "info" : { + "remote" : true, + "DateLogActionID" : "ABC+Time" + }, + "deviceID" : "online_with_doorsense", + "house" : { + "houseName" : "MockHouse", + "houseID" : "123" + } +}] diff --git a/tests/fixtures/august/get_activity.locking.json b/tests/fixtures/august/get_activity.locking.json new file mode 100644 index 00000000000..c1f07e47312 --- /dev/null +++ b/tests/fixtures/august/get_activity.locking.json @@ -0,0 +1,34 @@ +[{ + "entities" : { + "activity" : "mockActivity2", + "house" : "123", + "device" : "online_with_doorsense", + "callingUser" : "mockUserId2", + "otherUser" : "deleted" + }, + "callingUser" : { + "LastName" : "elven princess", + "UserID" : "mockUserId2", + "FirstName" : "Your favorite" + }, + "otherUser" : { + "LastName" : "User", + "UserName" : "deleteduser", + "FirstName" : "Unknown", + "UserID" : "deleted", + "PhoneNo" : "deleted" + }, + "deviceType" : "lock", + "deviceName" : "MockHouseTDoor", + "action" : "locking", + "dateTime" : 1582007218000, + "info" : { + "remote" : true, + "DateLogActionID" : "ABC+Time" + }, + "deviceID" : "online_with_doorsense", + "house" : { + "houseName" : "MockHouse", + "houseID" : "123" + } +}] diff --git a/tests/fixtures/august/get_activity.unlocking.json b/tests/fixtures/august/get_activity.unlocking.json new file mode 100644 index 00000000000..788a69164aa --- /dev/null +++ b/tests/fixtures/august/get_activity.unlocking.json @@ -0,0 +1,34 @@ +[{ + "entities" : { + "activity" : "mockActivity2", + "house" : "123", + "device" : "online_with_doorsense", + "callingUser" : "mockUserId2", + "otherUser" : "deleted" + }, + "callingUser" : { + "LastName" : "elven princess", + "UserID" : "mockUserId2", + "FirstName" : "Your favorite" + }, + "otherUser" : { + "LastName" : "User", + "UserName" : "deleteduser", + "FirstName" : "Unknown", + "UserID" : "deleted", + "PhoneNo" : "deleted" + }, + "deviceType" : "lock", + "deviceName" : "MockHouseTDoor", + "action" : "unlocking", + "dateTime" : 1582007218000, + "info" : { + "remote" : true, + "DateLogActionID" : "ABC+Time" + }, + "deviceID" : "online_with_doorsense", + "house" : { + "houseName" : "MockHouse", + "houseID" : "123" + } +}] From 5d85983b09f6eddcf69ab1eef6a547319590aa54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:49:05 -1000 Subject: [PATCH 444/818] Update google assistant locks to support locking, unlocking, jammed (#52820) --- .../components/google_assistant/trait.py | 7 +++- .../components/google_assistant/test_trait.py | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 0c547f18741..a710010bb8d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -24,6 +24,7 @@ from homeassistant.components import ( ) from homeassistant.components.climate import const as climate from homeassistant.components.humidifier import const as humidifier +from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -1101,7 +1102,11 @@ class LockUnlockTrait(_Trait): def query_attributes(self): """Return LockUnlock query attributes.""" - return {"isLocked": self.state.state == STATE_LOCKED} + if self.state.state == STATE_JAMMED: + return {"isJammed": True} + + # If its unlocking its not yet unlocked so we consider is locked + return {"isLocked": self.state.state in (STATE_UNLOCKING, STATE_LOCKED)} async def execute(self, command, data, params, challenge): """Execute an LockUnlock command.""" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e2821d207d5..11677ed67d1 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1042,6 +1042,45 @@ async def test_lock_unlock_lock(hass): assert calls[0].data == {ATTR_ENTITY_ID: "lock.front_door"} +async def test_lock_unlock_unlocking(hass): + """Test LockUnlock trait locking support for lock domain.""" + assert helpers.get_google_type(lock.DOMAIN, None) is not None + assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) + assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, None) + + trt = trait.LockUnlockTrait( + hass, State("lock.front_door", lock.STATE_UNLOCKING), PIN_CONFIG + ) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == {"isLocked": True} + + +async def test_lock_unlock_lock_jammed(hass): + """Test LockUnlock trait locking support for lock domain that jams.""" + assert helpers.get_google_type(lock.DOMAIN, None) is not None + assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) + assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, None) + + trt = trait.LockUnlockTrait( + hass, State("lock.front_door", lock.STATE_JAMMED), PIN_CONFIG + ) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == {"isJammed": True} + + assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {"lock": True}) + + calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK) + + await trt.execute(trait.COMMAND_LOCKUNLOCK, PIN_DATA, {"lock": True}, {}) + + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "lock.front_door"} + + async def test_lock_unlock_unlock(hass): """Test LockUnlock trait unlocking support for lock domain.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None From ee242764a11b7dcd3628dbd951ed882e5d210596 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:50:21 -1000 Subject: [PATCH 445/818] Update template lock to support locking, unlocking, jammed (#52817) --- homeassistant/components/template/lock.py | 33 ++++++++-- tests/components/template/test_lock.py | 78 +++++++++++++++++++++++ 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index c4a3977a4db..55a568ed3c2 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -1,7 +1,13 @@ """Support for locks which integrates with other components.""" import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + PLATFORM_SCHEMA, + STATE_JAMMED, + STATE_LOCKING, + STATE_UNLOCKING, + LockEntity, +) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, @@ -9,6 +15,8 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, STATE_LOCKED, STATE_ON, + STATE_UNKNOWN, + STATE_UNLOCKED, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError @@ -105,7 +113,22 @@ class TemplateLock(TemplateEntity, LockEntity): @property def is_locked(self): """Return true if lock is locked.""" - return self._state + return self._state in ("true", STATE_ON, STATE_LOCKED) + + @property + def is_jammed(self): + """Return true if lock is jammed.""" + return self._state == STATE_JAMMED + + @property + def is_unlocking(self): + """Return true if lock is unlocking.""" + return self._state == STATE_UNLOCKING + + @property + def is_locking(self): + """Return true if lock is locking.""" + return self._state == STATE_LOCKING @callback def _update_state(self, result): @@ -115,14 +138,14 @@ class TemplateLock(TemplateEntity, LockEntity): return if isinstance(result, bool): - self._state = result + self._state = STATE_LOCKED if result else STATE_UNLOCKED return if isinstance(result, str): - self._state = result.lower() in ("true", STATE_ON, STATE_LOCKED) + self._state = result.lower() return - self._state = False + self._state = STATE_UNKNOWN async def async_added_to_hass(self): """Register callbacks.""" diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index a00ca3b7e91..2cbdf23190d 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -325,6 +325,84 @@ async def test_unlock_action(hass, calls): assert len(calls) == 1 +async def test_unlocking(hass, calls): + """Test unlocking.""" + assert await setup.async_setup_component( + hass, + lock.DOMAIN, + { + "lock": { + "platform": "template", + "value_template": "{{ states.input_select.test_state.state }}", + "lock": {"service": "test.automation"}, + "unlock": {"service": "test.automation"}, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("input_select.test_state", lock.STATE_UNLOCKING) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_UNLOCKING + + +async def test_locking(hass, calls): + """Test unlocking.""" + assert await setup.async_setup_component( + hass, + lock.DOMAIN, + { + "lock": { + "platform": "template", + "value_template": "{{ states.input_select.test_state.state }}", + "lock": {"service": "test.automation"}, + "unlock": {"service": "test.automation"}, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("input_select.test_state", lock.STATE_LOCKING) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_LOCKING + + +async def test_jammed(hass, calls): + """Test jammed.""" + assert await setup.async_setup_component( + hass, + lock.DOMAIN, + { + "lock": { + "platform": "template", + "value_template": "{{ states.input_select.test_state.state }}", + "lock": {"service": "test.automation"}, + "unlock": {"service": "test.automation"}, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("input_select.test_state", lock.STATE_JAMMED) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_JAMMED + + async def test_available_template_with_entities(hass): """Test availability templates with values from other entities.""" From 564a5054865615c58a20409638bc65d4c6e5354b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:55:04 -1000 Subject: [PATCH 446/818] Update homekit controller lock to support locking, unlocking, jammed (#52821) --- .../components/homekit_controller/lock.py | 56 +++++++++++++++++-- .../homekit_controller/test_lock.py | 20 +++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 09c02ce0ff9..3b6fb41f3a8 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -2,18 +2,28 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.lock import LockEntity -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.components.lock import STATE_JAMMED, LockEntity +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + STATE_LOCKED, + STATE_UNKNOWN, + STATE_UNLOCKED, +) from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity -STATE_JAMMED = "jammed" - -CURRENT_STATE_MAP = {0: STATE_UNLOCKED, 1: STATE_LOCKED, 2: STATE_JAMMED, 3: None} +CURRENT_STATE_MAP = { + 0: STATE_UNLOCKED, + 1: STATE_LOCKED, + 2: STATE_JAMMED, + 3: STATE_UNKNOWN, +} TARGET_STATE_MAP = {STATE_UNLOCKED: 0, STATE_LOCKED: 1} +REVERSED_TARGET_STATE_MAP = {v: k for k, v in TARGET_STATE_MAP.items()} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lock.""" @@ -46,8 +56,44 @@ class HomeKitLock(HomeKitEntity, LockEntity): def is_locked(self): """Return true if device is locked.""" value = self.service.value(CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE) + if CURRENT_STATE_MAP[value] == STATE_UNKNOWN: + return None return CURRENT_STATE_MAP[value] == STATE_LOCKED + @property + def is_locking(self): + """Return true if device is locking.""" + current_value = self.service.value( + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE + ) + target_value = self.service.value( + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE + ) + return ( + CURRENT_STATE_MAP[current_value] == STATE_UNLOCKED + and REVERSED_TARGET_STATE_MAP.get(target_value) == STATE_LOCKED + ) + + @property + def is_unlocking(self): + """Return true if device is unlocking.""" + current_value = self.service.value( + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE + ) + target_value = self.service.value( + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE + ) + return ( + CURRENT_STATE_MAP[current_value] == STATE_LOCKED + and REVERSED_TARGET_STATE_MAP.get(target_value) == STATE_UNLOCKED + ) + + @property + def is_jammed(self): + """Return true if device is jammed.""" + value = self.service.value(CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE) + return CURRENT_STATE_MAP[value] == STATE_JAMMED + async def async_lock(self, **kwargs): """Lock the device.""" await self._set_lock_state(STATE_LOCKED) diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index 197b7b3c3b9..15e645bf181 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -57,3 +57,23 @@ async def test_switch_read_lock_state(hass, utcnow): helper.characteristics[LOCK_TARGET_STATE].value = 1 state = await helper.poll_and_get_state() assert state.state == "locked" + + helper.characteristics[LOCK_CURRENT_STATE].value = 2 + helper.characteristics[LOCK_TARGET_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "jammed" + + helper.characteristics[LOCK_CURRENT_STATE].value = 3 + helper.characteristics[LOCK_TARGET_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "unknown" + + helper.characteristics[LOCK_CURRENT_STATE].value = 0 + helper.characteristics[LOCK_TARGET_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "locking" + + helper.characteristics[LOCK_CURRENT_STATE].value = 1 + helper.characteristics[LOCK_TARGET_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.state == "unlocking" From fe89603ee78e050e342c3151eed629b3e08603e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:55:19 -1000 Subject: [PATCH 447/818] Update homekit lock to support locking, unlocking, jammed (#52819) --- .../components/homekit/type_locks.py | 92 ++++++++++++------- tests/components/homekit/test_type_locks.py | 22 ++++- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 17e2eee46e8..3a10a0a2f5a 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -3,7 +3,14 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK -from homeassistant.components.lock import DOMAIN, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.components.lock import ( + DOMAIN, + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, +) from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import callback @@ -12,16 +19,37 @@ from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = { +HASS_TO_HOMEKIT_CURRENT = { STATE_UNLOCKED: 0, + STATE_UNLOCKING: 1, + STATE_LOCKING: 0, STATE_LOCKED: 1, - # Value 2 is Jammed which hass doesn't have a state for + STATE_JAMMED: 2, STATE_UNKNOWN: 3, } -HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} +HASS_TO_HOMEKIT_TARGET = { + STATE_UNLOCKED: 0, + STATE_UNLOCKING: 0, + STATE_LOCKING: 1, + STATE_LOCKED: 1, +} -STATE_TO_SERVICE = {STATE_LOCKED: "lock", STATE_UNLOCKED: "unlock"} +VALID_TARGET_STATES = {STATE_LOCKING, STATE_UNLOCKING, STATE_LOCKED, STATE_UNLOCKED} + +HOMEKIT_TO_HASS = { + 0: STATE_UNLOCKED, + 1: STATE_LOCKED, + 2: STATE_JAMMED, + 3: STATE_UNKNOWN, +} + +STATE_TO_SERVICE = { + STATE_LOCKING: "unlock", + STATE_LOCKED: "lock", + STATE_UNLOCKING: "lock", + STATE_UNLOCKED: "unlock", +} @TYPES.register("Lock") @@ -39,11 +67,11 @@ class Lock(HomeAccessory): serv_lock_mechanism = self.add_preload_service(SERV_LOCK) self.char_current_state = serv_lock_mechanism.configure_char( - CHAR_LOCK_CURRENT_STATE, value=HASS_TO_HOMEKIT[STATE_UNKNOWN] + CHAR_LOCK_CURRENT_STATE, value=HASS_TO_HOMEKIT_CURRENT[STATE_UNKNOWN] ) self.char_target_state = serv_lock_mechanism.configure_char( CHAR_LOCK_TARGET_STATE, - value=HASS_TO_HOMEKIT[STATE_LOCKED], + value=HASS_TO_HOMEKIT_CURRENT[STATE_LOCKED], setter_callback=self.set_state, ) self.async_update_state(state) @@ -52,12 +80,9 @@ class Lock(HomeAccessory): """Set lock state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set state to %d", self.entity_id, value) - hass_value = HOMEKIT_TO_HASS.get(value) + hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - if self.char_current_state.value != value: - self.char_current_state.set_value(value) - params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code @@ -67,25 +92,28 @@ class Lock(HomeAccessory): def async_update_state(self, new_state): """Update lock after state changed.""" hass_state = new_state.state - if hass_state in HASS_TO_HOMEKIT: - current_lock_state = HASS_TO_HOMEKIT[hass_state] - _LOGGER.debug( - "%s: Updated current state to %s (%d)", - self.entity_id, - hass_state, - current_lock_state, - ) - # LockTargetState only supports locked and unlocked - # Must set lock target state before current state - # or there will be no notification - if ( - hass_state in (STATE_LOCKED, STATE_UNLOCKED) - and self.char_target_state.value != current_lock_state - ): - self.char_target_state.set_value(current_lock_state) + current_lock_state = HASS_TO_HOMEKIT_CURRENT.get( + hass_state, HASS_TO_HOMEKIT_CURRENT[STATE_UNKNOWN] + ) + target_lock_state = HASS_TO_HOMEKIT_TARGET.get(hass_state) + _LOGGER.debug( + "%s: Updated current state to %s (current=%d) (target=%s)", + self.entity_id, + hass_state, + current_lock_state, + target_lock_state, + ) + # LockTargetState only supports locked and unlocked + # Must set lock target state before current state + # or there will be no notification + if ( + target_lock_state is not None + and self.char_target_state.value != target_lock_state + ): + self.char_target_state.set_value(target_lock_state) - # Set lock current state ONLY after ensuring that - # target state is correct or there will be no - # notification - if self.char_current_state.value != current_lock_state: - self.char_current_state.set_value(current_lock_state) + # Set lock current state ONLY after ensuring that + # target state is correct or there will be no + # notification + if self.char_current_state.value != current_lock_state: + self.char_current_state.set_value(current_lock_state) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index b2bb9b4736e..e47f4dfac71 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -3,7 +3,12 @@ import pytest from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_locks import Lock -from homeassistant.components.lock import DOMAIN +from homeassistant.components.lock import ( + DOMAIN, + STATE_JAMMED, + STATE_LOCKING, + STATE_UNLOCKING, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -37,11 +42,26 @@ async def test_lock_unlock(hass, hk_driver, events): assert acc.char_current_state.value == 1 assert acc.char_target_state.value == 1 + hass.states.async_set(entity_id, STATE_LOCKING) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 1 + hass.states.async_set(entity_id, STATE_UNLOCKED) await hass.async_block_till_done() assert acc.char_current_state.value == 0 assert acc.char_target_state.value == 0 + hass.states.async_set(entity_id, STATE_UNLOCKING) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 0 + + hass.states.async_set(entity_id, STATE_JAMMED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 2 + assert acc.char_target_state.value == 0 + hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() assert acc.char_current_state.value == 3 From 8a7cb389ed3afdaa0ed497abd44466420c209267 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 21 Jul 2021 07:07:15 +0200 Subject: [PATCH 448/818] Drop support for fan speeds and support reverse (#53105) --- .../components/google_assistant/trait.py | 102 +++++++++-------- .../components/google_assistant/test_trait.py | 105 ++++++++---------- 2 files changed, 103 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index a710010bb8d..36222902296 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -142,6 +142,7 @@ COMMAND_MEDIA_SEEK_RELATIVE = f"{PREFIX_COMMANDS}mediaSeekRelative" COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition" COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle" COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop" +COMMAND_REVERSE = f"{PREFIX_COMMANDS}Reverse" COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity" COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel" @@ -1258,14 +1259,7 @@ class FanSpeedTrait(_Trait): """ name = TRAIT_FANSPEED - commands = [COMMAND_FANSPEED] - - speed_synonyms = { - fan.SPEED_OFF: ["stop", "off"], - fan.SPEED_LOW: ["slow", "low", "slowest", "lowest"], - fan.SPEED_MEDIUM: ["medium", "mid", "middle"], - fan.SPEED_HIGH: ["high", "max", "fast", "highest", "fastest", "maximum"], - } + commands = [COMMAND_FANSPEED, COMMAND_REVERSE] @staticmethod def supported(domain, features, device_class, _): @@ -1280,23 +1274,21 @@ class FanSpeedTrait(_Trait): """Return speed point and modes attributes for a sync request.""" domain = self.state.domain speeds = [] - reversible = False + result = {} if domain == fan.DOMAIN: - # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) - modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, []) - for mode in modes: - speed = { - "speed_name": mode, - "speed_values": [ - {"speed_synonym": self.speed_synonyms.get(mode), "lang": "en"} - ], - } - speeds.append(speed) reversible = bool( self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & fan.SUPPORT_DIRECTION ) + + result.update( + { + "reversible": reversible, + "supportsFanSpeedPercent": True, + } + ) + elif domain == climate.DOMAIN: modes = self.state.attributes.get(climate.ATTR_FAN_MODES, []) for mode in modes: @@ -1306,32 +1298,32 @@ class FanSpeedTrait(_Trait): } speeds.append(speed) - return { - "availableFanSpeeds": {"speeds": speeds, "ordered": True}, - "reversible": reversible, - "supportsFanSpeedPercent": True, - } + result.update( + { + "reversible": False, + "availableFanSpeeds": {"speeds": speeds, "ordered": True}, + } + ) + + return result def query_attributes(self): """Return speed point and modes query attributes.""" + attrs = self.state.attributes domain = self.state.domain response = {} if domain == climate.DOMAIN: - speed = attrs.get(climate.ATTR_FAN_MODE) - if speed is not None: - response["currentFanSpeedSetting"] = speed + speed = attrs.get(climate.ATTR_FAN_MODE) or "off" + response["currentFanSpeedSetting"] = speed + if domain == fan.DOMAIN: - speed = attrs.get(fan.ATTR_SPEED) percent = attrs.get(fan.ATTR_PERCENTAGE) or 0 - if speed is not None: - response["on"] = speed != fan.SPEED_OFF - response["currentFanSpeedSetting"] = speed - if percent is not None: - response["currentFanSpeedPercent"] = percent + response["currentFanSpeedPercent"] = percent + return response - async def execute(self, command, data, params, challenge): + async def execute_fanspeed(self, data, params): """Execute an SetFanSpeed command.""" domain = self.state.domain if domain == climate.DOMAIN: @@ -1345,25 +1337,43 @@ class FanSpeedTrait(_Trait): blocking=True, context=data.context, ) - if domain == fan.DOMAIN: - service_params = { - ATTR_ENTITY_ID: self.state.entity_id, - } - if "fanSpeedPercent" in params: - service = fan.SERVICE_SET_PERCENTAGE - service_params[fan.ATTR_PERCENTAGE] = params["fanSpeedPercent"] - else: - service = fan.SERVICE_SET_SPEED - service_params[fan.ATTR_SPEED] = params["fanSpeed"] + if domain == fan.DOMAIN: await self.hass.services.async_call( fan.DOMAIN, - service, - service_params, + fan.SERVICE_SET_PERCENTAGE, + { + ATTR_ENTITY_ID: self.state.entity_id, + fan.ATTR_PERCENTAGE: params["fanSpeedPercent"], + }, blocking=True, context=data.context, ) + async def execute_reverse(self, data, params): + """Execute a Reverse command.""" + domain = self.state.domain + if domain == fan.DOMAIN: + if self.state.attributes.get(fan.ATTR_DIRECTION) == fan.DIRECTION_FORWARD: + direction = fan.DIRECTION_REVERSE + else: + direction = fan.DIRECTION_FORWARD + + await self.hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_DIRECTION, + {ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_DIRECTION: direction}, + blocking=True, + context=data.context, + ) + + async def execute(self, command, data, params, challenge): + """Execute a smart home command.""" + if command == COMMAND_FANSPEED: + await self.execute_fanspeed(data, params) + elif command == COMMAND_REVERSE: + await self.execute_reverse(data, params) + @register_trait class ModesTrait(_Trait): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 11677ed67d1..c57d894c36d 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1461,13 +1461,6 @@ async def test_fan_speed(hass): "fan.living_room_fan", fan.SPEED_HIGH, attributes={ - "speed_list": [ - fan.SPEED_OFF, - fan.SPEED_LOW, - fan.SPEED_MEDIUM, - fan.SPEED_HIGH, - ], - "speed": "low", "percentage": 33, "percentage_step": 1.0, }, @@ -1476,64 +1469,14 @@ async def test_fan_speed(hass): ) assert trt.sync_attributes() == { - "availableFanSpeeds": { - "ordered": True, - "speeds": [ - { - "speed_name": "off", - "speed_values": [{"speed_synonym": ["stop", "off"], "lang": "en"}], - }, - { - "speed_name": "low", - "speed_values": [ - { - "speed_synonym": ["slow", "low", "slowest", "lowest"], - "lang": "en", - } - ], - }, - { - "speed_name": "medium", - "speed_values": [ - {"speed_synonym": ["medium", "mid", "middle"], "lang": "en"} - ], - }, - { - "speed_name": "high", - "speed_values": [ - { - "speed_synonym": [ - "high", - "max", - "fast", - "highest", - "fastest", - "maximum", - ], - "lang": "en", - } - ], - }, - ], - }, "reversible": False, "supportsFanSpeedPercent": True, } assert trt.query_attributes() == { - "currentFanSpeedSetting": "low", - "on": True, "currentFanSpeedPercent": 33, } - assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": "medium"}) - - calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED) - await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeed": "medium"}, {}) - - assert len(calls) == 1 - assert calls[0].data == {"entity_id": "fan.living_room_fan", "speed": "medium"} - assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10}) calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE) @@ -1543,6 +1486,53 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} +@pytest.mark.parametrize( + "direction_state,direction_call", + [ + (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE), + (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD), + (None, fan.DIRECTION_FORWARD), + ], +) +async def test_fan_reverse(hass, direction_state, direction_call): + """Test FanSpeed trait speed control support for fan domain.""" + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_DIRECTION) + + trt = trait.FanSpeedTrait( + hass, + State( + "fan.living_room_fan", + fan.SPEED_HIGH, + attributes={ + "percentage": 33, + "percentage_step": 1.0, + "direction": direction_state, + "supported_features": fan.SUPPORT_DIRECTION, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "reversible": True, + "supportsFanSpeedPercent": True, + } + + assert trt.query_attributes() == { + "currentFanSpeedPercent": 33, + } + + assert trt.can_execute(trait.COMMAND_REVERSE, params={}) + await trt.execute(trait.COMMAND_REVERSE, BASIC_DATA, {}, {}) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "fan.living_room_fan", + "direction": direction_call, + } + + async def test_climate_fan_speed(hass): """Test FanSpeed trait speed control support for climate domain.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None @@ -1586,7 +1576,6 @@ async def test_climate_fan_speed(hass): ], }, "reversible": False, - "supportsFanSpeedPercent": True, } assert trt.query_attributes() == { From 90765132cca33bc247cad098dee4d1eca3447560 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 21 Jul 2021 01:08:08 -0400 Subject: [PATCH 449/818] Make additional input for zwave_js device triggers optional (#53134) --- .../components/zwave_js/device_trigger.py | 7 +- .../zwave_js/test_device_trigger.py | 187 +++++++++++++++++- 2 files changed, 182 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 6734afd10e2..6d1b611d14f 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -92,7 +92,7 @@ BASE_VALUE_NOTIFICATION_EVENT_SCHEMA = BASE_EVENT_SCHEMA.extend( vol.Required(ATTR_PROPERTY): vol.Any(int, str), vol.Required(ATTR_PROPERTY_KEY): vol.Any(None, int, str), vol.Required(ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(ATTR_VALUE): vol.Coerce(int), + vol.Optional(ATTR_VALUE): vol.Coerce(int), vol.Required(CONF_SUBTYPE): cv.string, } ) @@ -286,7 +286,8 @@ async def async_attach_trigger( copy_available_params( config, event_data, [ATTR_PROPERTY, ATTR_PROPERTY_KEY, ATTR_ENDPOINT] ) - event_data[ATTR_VALUE_RAW] = config[ATTR_VALUE] + if ATTR_VALUE in config: + event_data[ATTR_VALUE_RAW] = config[ATTR_VALUE] else: raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") @@ -366,6 +367,6 @@ async def async_get_trigger_capabilities( vol.Range(min=value.metadata.min, max=value.metadata.max), ) - return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})} + return {"extra_fields": vol.Schema({vol.Optional(ATTR_VALUE): value_schema})} return {} diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 2c4d8ce2b33..86e053a5882 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -68,6 +68,7 @@ async def test_if_notification_notification_fires( automation.DOMAIN, { automation.DOMAIN: [ + # event, type, label { "trigger": { "platform": "device", @@ -91,6 +92,27 @@ async def test_if_notification_notification_fires( }, }, }, + # no type, event, label + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.notification", + "command_class": CommandClass.NOTIFICATION.value, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.notification.notification2 - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, ] }, ) @@ -114,12 +136,17 @@ async def test_if_notification_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 2 assert calls[0].data[ "some" ] == "event.notification.notification - device - zwave_js_notification - {}".format( CommandClass.NOTIFICATION ) + assert calls[1].data[ + "some" + ] == "event.notification.notification2 - device - zwave_js_notification - {}".format( + CommandClass.NOTIFICATION + ) async def test_get_trigger_capabilities_notification_notification( @@ -166,6 +193,7 @@ async def test_if_entry_control_notification_fires( automation.DOMAIN, { automation.DOMAIN: [ + # event_type and data_type { "trigger": { "platform": "device", @@ -188,6 +216,27 @@ async def test_if_entry_control_notification_fires( }, }, }, + # no event_type and data_type + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.entry_control", + "command_class": CommandClass.ENTRY_CONTROL.value, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.notification.notification2 - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, ] }, ) @@ -205,12 +254,17 @@ async def test_if_entry_control_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 2 assert calls[0].data[ "some" ] == "event.notification.notification - device - zwave_js_notification - {}".format( CommandClass.ENTRY_CONTROL ) + assert calls[1].data[ + "some" + ] == "event.notification.notification2 - device - zwave_js_notification - {}".format( + CommandClass.ENTRY_CONTROL + ) async def test_get_trigger_capabilities_entry_control_notification( @@ -285,6 +339,7 @@ async def test_if_node_status_change_fires( automation.DOMAIN, { automation.DOMAIN: [ + # from { "trigger": { "platform": "device", @@ -305,6 +360,26 @@ async def test_if_node_status_change_fires( }, }, }, + # no from or to + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity_id, + "type": "state.node_status", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "state.node_status2 - " + "{{ trigger.platform}} - " + "{{ trigger.from_state.state }}" + ) + }, + }, + }, ] }, ) @@ -315,8 +390,9 @@ async def test_if_node_status_change_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 2 assert calls[0].data["some"] == "state.node_status - device - alive" + assert calls[1].data["some"] == "state.node_status2 - device - alive" async def test_get_trigger_capabilities_node_status( @@ -408,6 +484,7 @@ async def test_if_basic_value_notification_fires( automation.DOMAIN, { automation.DOMAIN: [ + # value { "trigger": { "platform": "device", @@ -433,6 +510,31 @@ async def test_if_basic_value_notification_fires( }, }, }, + # no value + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC.value, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.basic2 - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, ] }, ) @@ -465,12 +567,17 @@ async def test_if_basic_value_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 2 assert calls[0].data[ "some" ] == "event.value_notification.basic - device - zwave_js_value_notification - {}".format( CommandClass.BASIC ) + assert calls[1].data[ + "some" + ] == "event.value_notification.basic2 - device - zwave_js_value_notification - {}".format( + CommandClass.BASIC + ) async def test_get_trigger_capabilities_basic_value_notification( @@ -500,7 +607,7 @@ async def test_get_trigger_capabilities_basic_value_notification( ) == [ { "name": "value", - "required": True, + "optional": True, "type": "integer", "valueMin": 0, "valueMax": 255, @@ -542,6 +649,7 @@ async def test_if_central_scene_value_notification_fires( automation.DOMAIN, { automation.DOMAIN: [ + # value { "trigger": { "platform": "device", @@ -567,6 +675,31 @@ async def test_if_central_scene_value_notification_fires( }, }, }, + # no value + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.value_notification.central_scene", + "command_class": CommandClass.CENTRAL_SCENE.value, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.central_scene2 - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, ] }, ) @@ -606,12 +739,17 @@ async def test_if_central_scene_value_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 2 assert calls[0].data[ "some" ] == "event.value_notification.central_scene - device - zwave_js_value_notification - {}".format( CommandClass.CENTRAL_SCENE ) + assert calls[1].data[ + "some" + ] == "event.value_notification.central_scene2 - device - zwave_js_value_notification - {}".format( + CommandClass.CENTRAL_SCENE + ) async def test_get_trigger_capabilities_central_scene_value_notification( @@ -641,7 +779,7 @@ async def test_get_trigger_capabilities_central_scene_value_notification( ) == [ { "name": "value", - "required": True, + "optional": True, "type": "select", "options": [(0, "KeyPressed"), (1, "KeyReleased"), (2, "KeyHeldDown")], }, @@ -682,6 +820,7 @@ async def test_if_scene_activation_value_notification_fires( automation.DOMAIN, { automation.DOMAIN: [ + # value { "trigger": { "platform": "device", @@ -707,6 +846,31 @@ async def test_if_scene_activation_value_notification_fires( }, }, }, + # No value + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.value_notification.scene_activation", + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.scene_activation2 - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, ] }, ) @@ -739,12 +903,17 @@ async def test_if_scene_activation_value_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 2 assert calls[0].data[ "some" ] == "event.value_notification.scene_activation - device - zwave_js_value_notification - {}".format( CommandClass.SCENE_ACTIVATION ) + assert calls[1].data[ + "some" + ] == "event.value_notification.scene_activation2 - device - zwave_js_value_notification - {}".format( + CommandClass.SCENE_ACTIVATION + ) async def test_get_trigger_capabilities_scene_activation_value_notification( @@ -774,7 +943,7 @@ async def test_get_trigger_capabilities_scene_activation_value_notification( ) == [ { "name": "value", - "required": True, + "optional": True, "type": "integer", "valueMin": 1, "valueMax": 255, From 8a72e8df79883897c42ac9a1a6a743b64be654f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 21 Jul 2021 07:41:08 +0200 Subject: [PATCH 450/818] Convert Mill consumption attributes to sensors (#52311) --- .coveragerc | 1 + homeassistant/components/mill/__init__.py | 21 +++++- homeassistant/components/mill/climate.py | 21 +----- homeassistant/components/mill/const.py | 4 +- homeassistant/components/mill/sensor.py | 85 +++++++++++++++++++++++ 5 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/mill/sensor.py diff --git a/.coveragerc b/.coveragerc index 3b6c140c0f1..84577e99197 100644 --- a/.coveragerc +++ b/.coveragerc @@ -622,6 +622,7 @@ omit = homeassistant/components/mill/__init__.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py + homeassistant/components/mill/sensor.py homeassistant/components/minecraft_server/__init__.py homeassistant/components/minecraft_server/binary_sensor.py homeassistant/components/minecraft_server/const.py diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 115bb5eb33c..75422cd26e1 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,10 +1,29 @@ """The mill component.""" +from mill import Mill -PLATFORMS = ["climate"] +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +PLATFORMS = ["climate", "sensor"] async def async_setup_entry(hass, entry): """Set up the Mill heater.""" + mill_data_connection = Mill( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + websession=async_get_clientsession(hass), + ) + if not await mill_data_connection.connect(): + raise ConfigEntryNotReady + + await mill_data_connection.find_all_heaters() + + hass.data[DOMAIN] = mill_data_connection + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 2d591c67668..16c78329b0b 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -1,5 +1,4 @@ """Support for mill wifi-enabled home heaters.""" -from mill import Mill import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -12,15 +11,8 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_PASSWORD, - CONF_USERNAME, - TEMP_CELSIUS, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( ATTR_AWAY_TEMP, @@ -48,15 +40,8 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill climate.""" - mill_data_connection = Mill( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - websession=async_get_clientsession(hass), - ) - if not await mill_data_connection.connect(): - raise ConfigEntryNotReady - await mill_data_connection.find_all_heaters() + mill_data_connection = hass.data[DOMAIN] dev = [] for heater in mill_data_connection.heaters.values(): @@ -109,8 +94,6 @@ class MillHeater(ClimateEntity): "heating": self._heater.is_heating, "controlled_by_tibber": self._heater.tibber_control, "heater_generation": 1 if self._heater.is_gen1 else 2, - "consumption_today": self._heater.day_consumption, - "consumption_total": self._heater.year_consumption, } if self._heater.room: res["room"] = self._heater.room.name diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py index b0ba7065e0a..61171420e44 100644 --- a/homeassistant/components/mill/const.py +++ b/homeassistant/components/mill/const.py @@ -4,8 +4,10 @@ ATTR_AWAY_TEMP = "away_temp" ATTR_COMFORT_TEMP = "comfort_temp" ATTR_ROOM_NAME = "room_name" ATTR_SLEEP_TEMP = "sleep_temp" +CONSUMPTION_TODAY = "consumption_today" +CONSUMPTION_YEAR = "consumption_year" +DOMAIN = "mill" MANUFACTURER = "Mill" MAX_TEMP = 35 MIN_TEMP = 5 -DOMAIN = "mill" SERVICE_SET_ROOM_TEMP = "set_room_temperature" diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py new file mode 100644 index 00000000000..5b0cf7efd73 --- /dev/null +++ b/homeassistant/components/mill/sensor.py @@ -0,0 +1,85 @@ +"""Support for mill wifi-enabled home heaters.""" + +from homeassistant.components.sensor import ( + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, STATE_UNKNOWN +from homeassistant.util import dt as dt_util + +from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Mill sensor.""" + + mill_data_connection = hass.data[DOMAIN] + + dev = [] + for heater in mill_data_connection.heaters.values(): + for sensor_type in [CONSUMPTION_TODAY, CONSUMPTION_YEAR]: + dev.append( + MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) + ) + async_add_entities(dev) + + +class MillHeaterEnergySensor(SensorEntity): + """Representation of a Mill Sensor device.""" + + def __init__(self, heater, mill_data_connection, sensor_type): + """Initialize the sensor.""" + self._id = heater.device_id + self._conn = mill_data_connection + self._sensor_type = sensor_type + + self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" + self._attr_unique_id = f"{heater.device_id}_{sensor_type}" + self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_info = { + "identifiers": {(DOMAIN, heater.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": f"generation {1 if heater.is_gen1 else 2}", + } + if self._sensor_type == CONSUMPTION_TODAY: + self._attr_last_reset = dt_util.as_utc( + dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + ) + elif self._sensor_type == CONSUMPTION_YEAR: + self._attr_last_reset = dt_util.as_utc( + dt_util.now().replace( + month=1, day=1, hour=0, minute=0, second=0, microsecond=0 + ) + ) + + async def async_update(self): + """Retrieve latest state.""" + heater = await self._conn.update_device(self._id) + self._attr_available = heater.available + + if self._sensor_type == CONSUMPTION_TODAY: + _state = heater.day_consumption + elif self._sensor_type == CONSUMPTION_YEAR: + _state = heater.year_consumption + else: + _state = None + if _state is None: + self._attr_state = _state + return + + if self.state not in [STATE_UNKNOWN, None] and _state < self.state: + if self._sensor_type == CONSUMPTION_TODAY: + self._attr_last_reset = dt_util.as_utc( + dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + ) + elif self._sensor_type == CONSUMPTION_YEAR: + self._attr_last_reset = dt_util.as_utc( + dt_util.now().replace( + month=1, day=1, hour=0, minute=0, second=0, microsecond=0 + ) + ) + self._attr_state = _state From 2e2b340b1ea83f18085235ac31ee47782620ffbd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 21 Jul 2021 07:48:02 +0200 Subject: [PATCH 451/818] Set modbus entity to non-available unless scan_interval=0 (#53155) --- homeassistant/components/modbus/base_platform.py | 1 + tests/components/modbus/test_binary_sensor.py | 2 ++ tests/components/modbus/test_climate.py | 2 ++ tests/components/modbus/test_cover.py | 3 +++ tests/components/modbus/test_fan.py | 4 ++++ tests/components/modbus/test_light.py | 4 ++++ tests/components/modbus/test_sensor.py | 2 ++ tests/components/modbus/test_switch.py | 3 +++ 8 files changed, 21 insertions(+) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 39ec283519a..0b612c3ecf5 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -68,6 +68,7 @@ class BasePlatform(Entity): self._value = None self._available = True self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) + self._available = self._scan_interval == 0 self._call_active = False @abstractmethod diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index e9c178ff025..e77fd380a22 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_DEVICE_CLASS, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_SLAVE, STATE_OFF, STATE_ON, @@ -144,6 +145,7 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): { CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, } ] }, diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index b872f4fe302..97d2c32ba69 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -104,6 +104,7 @@ async def test_service_climate_update(hass, mock_pymodbus): CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, } ] } @@ -176,6 +177,7 @@ test_value.attributes = {ATTR_TEMPERATURE: 37} CONF_NAME: CLIMATE_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, + CONF_SCAN_INTERVAL: 0, } ], }, diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 37274603bee..8d7e7e39cf8 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -211,6 +211,7 @@ async def test_service_cover_update(hass, mock_pymodbus): CONF_STATE_CLOSING: 3, CONF_STATUS_REGISTER: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, } ] }, @@ -232,11 +233,13 @@ async def test_service_cover_move(hass, mock_pymodbus): CONF_NAME: COVER_NAME, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, }, { CONF_NAME: f"{COVER_NAME}2", CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, + CONF_SCAN_INTERVAL: 0, }, ] } diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 4eeb094130b..13714d6bd0e 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_TYPE, STATE_OFF, @@ -195,6 +196,7 @@ async def test_all_fan(hass, call_type, regs, verify, expected): { CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, + CONF_SCAN_INTERVAL: 0, } ] }, @@ -219,11 +221,13 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): CONF_NAME: FAN_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, }, { CONF_NAME: f"{FAN_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, }, ], diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index e962b69a2a6..c7b9b820934 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_LIGHTS, CONF_NAME, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_TYPE, STATE_OFF, @@ -195,6 +196,7 @@ async def test_all_light(hass, call_type, regs, verify, expected): { CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, + CONF_SCAN_INTERVAL: 0, } ] }, @@ -219,11 +221,13 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, }, { CONF_NAME: f"{LIGHT_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, }, ], diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index a3433a504b8..9b0c868f8cb 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, + CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, @@ -578,6 +579,7 @@ async def test_struct_sensor(hass, cfg, regs, expected): { CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, } ] }, diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index b31ca12c48b..c620429aad2 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -210,6 +210,7 @@ async def test_all_switch(hass, call_type, regs, verify, expected): { CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, + CONF_SCAN_INTERVAL: 0, } ] }, @@ -234,11 +235,13 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, }, { CONF_NAME: f"{SWITCH_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, }, ], From 73065037563e56deb154582e9323ddb1372ad1ba Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 21 Jul 2021 07:49:54 +0200 Subject: [PATCH 452/818] Calculate count automatically in modbus platforms (#53116) --- homeassistant/components/modbus/__init__.py | 12 +++---- homeassistant/components/modbus/const.py | 20 ++++++------ homeassistant/components/modbus/validators.py | 32 ++++++++++--------- tests/components/modbus/test_init.py | 14 ++++---- tests/components/modbus/test_sensor.py | 2 +- 5 files changed, 39 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 9f851b4a235..8936ffc32ac 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -114,11 +114,7 @@ from .const import ( MODBUS_DOMAIN as DOMAIN, ) from .modbus import async_modbus_setup -from .validators import ( - number_validator, - scan_interval_validator, - sensor_schema_validator, -) +from .validators import number_validator, scan_interval_validator, struct_validator _LOGGER = logging.getLogger(__name__) @@ -145,7 +141,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( CALL_TYPE_REGISTER_INPUT, ] ), - vol.Optional(CONF_COUNT, default=1): cv.positive_int, + vol.Optional(CONF_COUNT): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( [ DATA_TYPE_INT16, @@ -289,12 +285,12 @@ MODBUS_SCHEMA = vol.Schema( cv.ensure_list, [BINARY_SENSOR_SCHEMA] ), vol.Optional(CONF_CLIMATES): vol.All( - cv.ensure_list, [vol.All(CLIMATE_SCHEMA, sensor_schema_validator)] + cv.ensure_list, [vol.All(CLIMATE_SCHEMA, struct_validator)] ), vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]), vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.All(SENSOR_SCHEMA, sensor_schema_validator)] + cv.ensure_list, [vol.All(SENSOR_SCHEMA, struct_validator)] ), vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]), diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 62319bcde85..49b7683435e 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -111,16 +111,16 @@ DEFAULT_SCAN_INTERVAL = 15 # seconds DEFAULT_SLAVE = 1 DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_STRUCT_FORMAT = { - DATA_TYPE_INT16: "h", - DATA_TYPE_INT32: "i", - DATA_TYPE_INT64: "q", - DATA_TYPE_UINT16: "H", - DATA_TYPE_UINT32: "I", - DATA_TYPE_UINT64: "Q", - DATA_TYPE_FLOAT16: "e", - DATA_TYPE_FLOAT32: "f", - DATA_TYPE_FLOAT64: "d", - DATA_TYPE_STRING: "s", + DATA_TYPE_INT16: ["h", 1], + DATA_TYPE_INT32: ["i", 2], + DATA_TYPE_INT64: ["q", 4], + DATA_TYPE_UINT16: ["H", 1], + DATA_TYPE_UINT32: ["I", 2], + DATA_TYPE_UINT64: ["Q", 4], + DATA_TYPE_FLOAT16: ["e", 1], + DATA_TYPE_FLOAT32: ["f", 2], + DATA_TYPE_FLOAT64: ["d", 4], + DATA_TYPE_STRING: ["s", 1], } DEFAULT_TEMP_UNIT = "C" MODBUS_DOMAIN = "modbus" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index e2777547ce4..9d72b611adc 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -40,7 +40,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -old_data_types = { +OLD_DATA_TYPES = { DATA_TYPE_INT: { 1: DATA_TYPE_INT16, 2: DATA_TYPE_INT32, @@ -59,39 +59,41 @@ old_data_types = { } -def sensor_schema_validator(config): +def struct_validator(config): """Sensor schema validator.""" data_type = config[CONF_DATA_TYPE] - count = config[CONF_COUNT] + count = config.get(CONF_COUNT, 1) name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) swap_type = config.get(CONF_SWAP) if data_type in [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]: - error = f"{name} {name} with {data_type} is not valid, trying to convert" + error = f"{name} with {data_type} is not valid, trying to convert" _LOGGER.warning(error) try: - data_type = old_data_types[data_type][count] + data_type = OLD_DATA_TYPES[data_type][config.get(CONF_COUNT, 1)] except KeyError as exp: - raise vol.Invalid("cannot convert automatically") from exp - + error = f"{name} cannot convert automatically {data_type}" + raise vol.Invalid(error) from exp if config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: - try: - structure = f">{DEFAULT_STRUCT_FORMAT[data_type]}" - except KeyError as exp: - raise vol.Invalid(f"Modbus error {data_type} unknown in {name}") from exp + if structure: + error = f"{name} structure: cannot be mixed with {data_type}" + raise vol.Invalid(error) + structure = f">{DEFAULT_STRUCT_FORMAT[data_type][0]}" + if CONF_COUNT not in config: + config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type][1] else: if not structure: - raise vol.Invalid( - f"Error in sensor {config[CONF_NAME]}. The `{CONF_STRUCTURE}` field can not be empty " - f"if the parameter `{CONF_DATA_TYPE}` is set to the `{DATA_TYPE_CUSTOM}`" + error = ( + f"Error in sensor {name}. The `{CONF_STRUCTURE}` field can not be empty" ) - + raise vol.Invalid(error) try: size = struct.calcsize(structure) except struct.error as err: raise vol.Invalid(f"Error in {name} structure: {str(err)}") from err + count = config.get(CONF_COUNT, 1) bytecount = count * 2 if bytecount != size: raise vol.Invalid( diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index ec68b98efa0..8b8d063bf02 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -55,7 +55,7 @@ from homeassistant.components.modbus.const import ( ) from homeassistant.components.modbus.validators import ( number_validator, - sensor_schema_validator, + struct_validator, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( @@ -144,12 +144,12 @@ async def test_number_validator(): }, ], ) -async def test_ok_sensor_schema_validator(do_config): +async def test_ok_struct_validator(do_config): """Test struct validator.""" try: - sensor_schema_validator(do_config) + struct_validator(do_config) except vol.Invalid: - pytest.fail("Sensor_schema_validator unexpected exception") + pytest.fail("struct_validator unexpected exception") @pytest.mark.parametrize( @@ -186,13 +186,13 @@ async def test_ok_sensor_schema_validator(do_config): }, ], ) -async def test_exception_sensor_schema_validator(do_config): +async def test_exception_struct_validator(do_config): """Test struct validator.""" try: - sensor_schema_validator(do_config) + struct_validator(do_config) except vol.Invalid: return - pytest.fail("Sensor_schema_validator missing exception") + pytest.fail("struct_validator missing exception") @pytest.mark.parametrize( diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 9b0c868f8cb..f01a3ef9da5 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -174,7 +174,7 @@ async def test_config_sensor(hass, mock_modbus): CONF_SWAP: CONF_SWAP_NONE, CONF_STRUCTURE: "", }, - "Error in sensor test_sensor. The `structure` field can not be empty if the parameter `data_type` is set to the `custom`", + "Error in sensor test_sensor. The `structure` field can not be empty", ), ( { From 2cf930f3bd1505ac48030854f2d82dafe7729d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 21 Jul 2021 08:46:01 +0200 Subject: [PATCH 453/818] Netatmo, use nameclass (#53247) --- homeassistant/components/netatmo/sensor.py | 344 ++++++++++++--------- 1 file changed, 206 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 917d04d1b4d..46bb06149cd 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,5 +1,8 @@ """Support for the Netatmo Weather Service.""" +from __future__ import annotations + import logging +from typing import NamedTuple from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -51,136 +54,172 @@ SUPPORTED_PUBLIC_SENSOR_TYPES = [ "sum_rain_24", ] -# sensor type: [name, netatmo name, unit of measurement, icon, device class, enable default] -SENSOR_TYPES = { - "temperature": [ + +class SensorMetadata(NamedTuple): + """Metadata for an individual sensor.""" + + name: str + netatmo_name: str + enable_default: bool + unit: str | None = None + icon: str | None = None + device_class: str | None = None + + +SENSOR_TYPES: dict[str, SensorMetadata] = { + "temperature": SensorMetadata( "Temperature", - "Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - True, - ], - "temp_trend": [ + netatmo_name="Temperature", + enable_default=True, + unit=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + "temp_trend": SensorMetadata( "Temperature trend", - "temp_trend", - None, - "mdi:trending-up", - None, - False, - ], - "co2": [ + netatmo_name="temp_trend", + enable_default=False, + icon="mdi:trending-up", + ), + "co2": SensorMetadata( "CO2", - "CO2", - CONCENTRATION_PARTS_PER_MILLION, - None, - DEVICE_CLASS_CO2, - True, - ], - "pressure": [ + netatmo_name="CO2", + unit=CONCENTRATION_PARTS_PER_MILLION, + enable_default=True, + device_class=DEVICE_CLASS_CO2, + ), + "pressure": SensorMetadata( "Pressure", - "Pressure", - PRESSURE_MBAR, - None, - DEVICE_CLASS_PRESSURE, - True, - ], - "pressure_trend": [ + netatmo_name="Pressure", + enable_default=True, + unit=PRESSURE_MBAR, + device_class=DEVICE_CLASS_PRESSURE, + ), + "pressure_trend": SensorMetadata( "Pressure trend", - "pressure_trend", - None, - "mdi:trending-up", - None, - False, - ], - "noise": ["Noise", "Noise", SOUND_PRESSURE_DB, "mdi:volume-high", None, True], - "humidity": ["Humidity", "Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, True], - "rain": ["Rain", "Rain", LENGTH_MILLIMETERS, "mdi:weather-rainy", None, True], - "sum_rain_1": [ + netatmo_name="pressure_trend", + enable_default=False, + icon="mdi:trending-up", + ), + "noise": SensorMetadata( + "Noise", + netatmo_name="Noise", + enable_default=True, + unit=SOUND_PRESSURE_DB, + icon="mdi:volume-high", + ), + "humidity": SensorMetadata( + "Humidity", + netatmo_name="Humidity", + enable_default=True, + unit=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + ), + "rain": SensorMetadata( + "Rain", + netatmo_name="Rain", + enable_default=True, + unit=LENGTH_MILLIMETERS, + icon="mdi:weather-rainy", + ), + "sum_rain_1": SensorMetadata( "Rain last hour", - "sum_rain_1", - LENGTH_MILLIMETERS, - "mdi:weather-rainy", - None, - False, - ], - "sum_rain_24": [ + enable_default=False, + netatmo_name="sum_rain_1", + unit=LENGTH_MILLIMETERS, + icon="mdi:weather-rainy", + ), + "sum_rain_24": SensorMetadata( "Rain today", - "sum_rain_24", - LENGTH_MILLIMETERS, - "mdi:weather-rainy", - None, - True, - ], - "battery_percent": [ + enable_default=True, + netatmo_name="sum_rain_24", + unit=LENGTH_MILLIMETERS, + icon="mdi:weather-rainy", + ), + "battery_percent": SensorMetadata( "Battery Percent", - "battery_percent", - PERCENTAGE, - None, - DEVICE_CLASS_BATTERY, - True, - ], - "windangle": ["Direction", "WindAngle", None, "mdi:compass-outline", None, True], - "windangle_value": [ + netatmo_name="battery_percent", + enable_default=True, + unit=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + "windangle": SensorMetadata( + "Direction", + netatmo_name="WindAngle", + enable_default=True, + icon="mdi:compass-outline", + ), + "windangle_value": SensorMetadata( "Angle", - "WindAngle", - DEGREE, - "mdi:compass-outline", - None, - False, - ], - "windstrength": [ + netatmo_name="WindAngle", + enable_default=False, + unit=DEGREE, + icon="mdi:compass-outline", + ), + "windstrength": SensorMetadata( "Wind Strength", - "WindStrength", - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - None, - True, - ], - "gustangle": [ + netatmo_name="WindStrength", + enable_default=True, + unit=SPEED_KILOMETERS_PER_HOUR, + icon="mdi:weather-windy", + ), + "gustangle": SensorMetadata( "Gust Direction", - "GustAngle", - None, - "mdi:compass-outline", - None, - False, - ], - "gustangle_value": [ + netatmo_name="GustAngle", + enable_default=False, + icon="mdi:compass-outline", + ), + "gustangle_value": SensorMetadata( "Gust Angle", - "GustAngle", - DEGREE, - "mdi:compass-outline", - None, - False, - ], - "guststrength": [ + netatmo_name="GustAngle", + enable_default=False, + unit=DEGREE, + icon="mdi:compass-outline", + ), + "guststrength": SensorMetadata( "Gust Strength", - "GustStrength", - SPEED_KILOMETERS_PER_HOUR, - "mdi:weather-windy", - None, - False, - ], - "reachable": ["Reachability", "reachable", None, "mdi:signal", None, False], - "rf_status": ["Radio", "rf_status", None, "mdi:signal", None, False], - "rf_status_lvl": [ + netatmo_name="GustStrength", + enable_default=False, + unit=SPEED_KILOMETERS_PER_HOUR, + icon="mdi:weather-windy", + ), + "reachable": SensorMetadata( + "Reachability", + netatmo_name="reachable", + enable_default=False, + icon="mdi:signal", + ), + "rf_status": SensorMetadata( + "Radio", + netatmo_name="rf_status", + enable_default=False, + icon="mdi:signal", + ), + "rf_status_lvl": SensorMetadata( "Radio Level", - "rf_status", - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - None, - DEVICE_CLASS_SIGNAL_STRENGTH, - False, - ], - "wifi_status": ["Wifi", "wifi_status", None, "mdi:wifi", None, False], - "wifi_status_lvl": [ + netatmo_name="rf_status", + enable_default=False, + unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + ), + "wifi_status": SensorMetadata( + "Wifi", + netatmo_name="wifi_status", + enable_default=False, + icon="mdi:wifi", + ), + "wifi_status_lvl": SensorMetadata( "Wifi Level", - "wifi_status", - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - None, - DEVICE_CLASS_SIGNAL_STRENGTH, - False, - ], - "health_idx": ["Health", "health_idx", None, "mdi:cloud", None, True], + netatmo_name="wifi_status", + enable_default=False, + unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + ), + "health_idx": SensorMetadata( + "Health", + enable_default=True, + netatmo_name="health_idx", + icon="mdi:cloud", + ), } MODULE_TYPE_OUTDOOR = "NAModule1" @@ -188,11 +227,41 @@ MODULE_TYPE_WIND = "NAModule2" MODULE_TYPE_RAIN = "NAModule3" MODULE_TYPE_INDOOR = "NAModule4" + +class BatteryData(NamedTuple): + """Metadata for a batter.""" + + full: int + high: int + medium: int + low: int + + BATTERY_VALUES = { - MODULE_TYPE_WIND: {"Full": 5590, "High": 5180, "Medium": 4770, "Low": 4360}, - MODULE_TYPE_RAIN: {"Full": 5500, "High": 5000, "Medium": 4500, "Low": 4000}, - MODULE_TYPE_INDOOR: {"Full": 5500, "High": 5280, "Medium": 4920, "Low": 4560}, - MODULE_TYPE_OUTDOOR: {"Full": 5500, "High": 5000, "Medium": 4500, "Low": 4000}, + MODULE_TYPE_WIND: BatteryData( + full=5590, + high=5180, + medium=4770, + low=4360, + ), + MODULE_TYPE_RAIN: BatteryData( + full=5500, + high=5000, + medium=4500, + low=4000, + ), + MODULE_TYPE_INDOOR: BatteryData( + full=5500, + high=5280, + medium=4920, + low=4560, + ), + MODULE_TYPE_OUTDOOR: BatteryData( + full=5500, + high=5000, + medium=4500, + low=4000, + ), } PUBLIC = "public" @@ -331,6 +400,8 @@ class NetatmoSensor(NetatmoBase, SensorEntity): """Initialize the sensor.""" super().__init__(data_handler) + metadata: SensorMetadata = SENSOR_TYPES[sensor_type] + self._data_classes.append( {"name": data_class_name, SIGNAL_NAME: data_class_name} ) @@ -353,16 +424,14 @@ class NetatmoSensor(NetatmoBase, SensorEntity): f"{module_info.get('module_name', device['type'])}" ) - self._attr_name = ( - f"{MANUFACTURER} {self._device_name} {SENSOR_TYPES[sensor_type][0]}" - ) + self._attr_name = f"{MANUFACTURER} {self._device_name} {metadata.name}" self.type = sensor_type - self._attr_device_class = SENSOR_TYPES[self.type][4] - self._attr_icon = SENSOR_TYPES[self.type][3] - self._attr_unit_of_measurement = SENSOR_TYPES[self.type][2] + self._attr_device_class = metadata.device_class + self._attr_icon = metadata.icon + self._attr_unit_of_measurement = metadata.unit self._model = device["type"] self._attr_unique_id = f"{self._id}-{self.type}" - self._attr_entity_registry_enabled_default = SENSOR_TYPES[self.type][5] + self._attr_entity_registry_enabled_default = metadata.enable_default @property def available(self): @@ -395,7 +464,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): return try: - state = data[SENSOR_TYPES[self.type][1]] + state = data[SENSOR_TYPES[self.type].netatmo_name] if self.type in {"temperature", "pressure", "sum_rain_1"}: self._attr_state = round(state, 1) elif self.type in {"windangle_value", "gustangle_value"}: @@ -449,15 +518,15 @@ def process_angle(angle: int) -> str: def process_battery(data: int, model: str) -> str: """Process battery data and return string for display.""" - values = BATTERY_VALUES[model] + battery_data = BATTERY_VALUES[model] - if data >= values["Full"]: + if data >= battery_data.full: return "Full" - if data >= values["High"]: + if data >= battery_data.high: return "High" - if data >= values["Medium"]: + if data >= battery_data.medium: return "Medium" - if data >= values["Low"]: + if data >= battery_data.low: return "Low" return "Very Low" @@ -518,6 +587,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): SIGNAL_NAME: self._signal_name, } ) + metadata: SensorMetadata = SENSOR_TYPES[sensor_type] self.type = sensor_type self.area = area @@ -525,12 +595,10 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = ( - f"{MANUFACTURER} {self._device_name} {SENSOR_TYPES[self.type][0]}" - ) - self._attr_device_class = SENSOR_TYPES[self.type][4] - self._attr_icon = SENSOR_TYPES[self.type][3] - self._attr_unit_of_measurement = SENSOR_TYPES[self.type][2] + self._attr_name = f"{MANUFACTURER} {self._device_name} {metadata.name}" + self._attr_device_class = metadata.device_class + self._attr_icon = metadata.icon + self._attr_unit_of_measurement = metadata.unit self._show_on_map = area.show_on_map self._attr_unique_id = f"{self._device_name.replace(' ', '-')}-{self.type}" self._model = PUBLIC From 930db7167e3bf6e140ecfda8743626fdb6c06ac9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 02:53:53 -0400 Subject: [PATCH 454/818] Code quality improvements for goalzero (#53260) --- homeassistant/components/goalzero/__init__.py | 42 +++++++++++-------- .../components/goalzero/binary_sensor.py | 13 +++--- .../components/goalzero/config_flow.py | 4 +- homeassistant/components/goalzero/const.py | 4 -- homeassistant/components/goalzero/sensor.py | 9 ++-- homeassistant/components/goalzero/switch.py | 17 ++++---- 6 files changed, 46 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index fe2d5cc695e..308934819cd 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -1,4 +1,6 @@ """The Goal Zero Yeti integration.""" +from __future__ import annotations + import logging from goalzero import Yeti, exceptions @@ -7,10 +9,20 @@ from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSO from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, + CONF_HOST, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -41,7 +53,7 @@ async def async_setup_entry(hass, entry): try: await api.init_connect() except exceptions.ConnectError as ex: - _LOGGER.warning("Failed to connect: %s", ex) + _LOGGER.warning("Failed to connect to device %s", ex) raise ConfigEntryNotReady from ex async def async_update_data(): @@ -88,23 +100,19 @@ class YetiEntity(CoordinatorEntity): self.api = api self._name = name self._server_unique_id = server_unique_id - self._device_class = None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" - info = { - "identifiers": {(DOMAIN, self._server_unique_id)}, - "manufacturer": "Goal Zero", - "name": self._name, - } + model = sw_version = None if self.api.sysdata: - info["model"] = self.api.sysdata["model"] + model = self.api.sysdata[ATTR_MODEL] if self.api.data: - info["sw_version"] = self.api.data["firmwareVersion"] - return info - - @property - def device_class(self): - """Return the class of this device.""" - return self._device_class + sw_version = self.api.data["firmwareVersion"] + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, + ATTR_MANUFACTURER: "Goal Zero", + ATTR_NAME: self._name, + ATTR_MODEL: str(model), + ATTR_SW_VERSION: str(sw_version), + } diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index aec9fdb0354..74776eb51b5 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -12,7 +12,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Goal Zero Yeti sensor.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] - sensors = [ + async_add_entities( YetiBinarySensor( goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], @@ -21,8 +21,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entry.entry_id, ) for sensor_name in BINARY_SENSOR_DICT - ] - async_add_entities(sensors) + ) class YetiBinarySensor(YetiEntity, BinarySensorEntity): @@ -47,23 +46,23 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): self._device_class = variable_info[1] @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._name} {self._condition_name}" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id of the sensor.""" return f"{self._server_unique_id}/{self._condition_name}" @property - def is_on(self): + def is_on(self) -> bool: """Return if the service is on.""" if self.api.data: return self.api.data[self._condition] == 1 return False @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" return self._icon diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 50450f95a69..4c525de9c7d 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -63,7 +63,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -98,7 +98,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_try_connect(self, host): + async def _async_try_connect(self, host) -> tuple: """Try connecting to Goal Zero Yeti.""" try: session = async_get_clientsession(self.hass) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index 4c860a5e0e4..da4a6ee4ad6 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -34,10 +34,6 @@ from homeassistant.const import ( ATTRIBUTION = "Data provided by Goal Zero" ATTR_DEFAULT_ENABLED = "default_enabled" -CONF_IDENTIFIERS = "identifiers" -CONF_MANUFACTURER = "manufacturer" -CONF_MODEL = "model" -CONF_SW_VERSION = "sw_version" DATA_KEY_COORDINATOR = "coordinator" DOMAIN = "goalzero" DEFAULT_NAME = "Yeti" diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index ccb39ae0813..f64d6d772c8 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -1,4 +1,6 @@ """Support for Goal Zero Yeti Sensors.""" +from __future__ import annotations + from homeassistant.components.sensor import ATTR_LAST_RESET, ATTR_STATE_CLASS from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -40,20 +42,19 @@ class YetiSensor(YetiEntity): def __init__(self, api, coordinator, name, sensor_name, server_unique_id): """Initialize a Goal Zero Yeti sensor.""" super().__init__(api, coordinator, name, server_unique_id) - self._condition = sensor_name - sensor = SENSOR_DICT[sensor_name] self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" self._attr_unique_id = f"{self._server_unique_id}/{sensor_name}" self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) - self._device_class = sensor.get(ATTR_DEVICE_CLASS) + self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) self._attr_last_reset = sensor.get(ATTR_LAST_RESET) self._attr_state_class = sensor.get(ATTR_STATE_CLASS) @property - def state(self): + def state(self) -> str | None: """Return the state.""" if self.api.data: return self.api.data.get(self._condition) + return None diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index dd4c9deae3e..92808ef5f43 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -1,4 +1,6 @@ """Support for Goal Zero Yeti Switches.""" +from __future__ import annotations + from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME @@ -10,7 +12,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Goal Zero Yeti switch.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] - switches = [ + async_add_entities( YetiSwitch( goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], @@ -19,8 +21,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entry.entry_id, ) for switch_name in SWITCH_DICT - ] - async_add_entities(switches) + ) class YetiSwitch(YetiEntity, SwitchEntity): @@ -36,27 +37,25 @@ class YetiSwitch(YetiEntity, SwitchEntity): ): """Initialize a Goal Zero Yeti switch.""" super().__init__(api, coordinator, name, server_unique_id) - self._condition = switch_name - self._condition_name = SWITCH_DICT[switch_name] @property - def name(self): + def name(self) -> str: """Return the name of the switch.""" return f"{self._name} {self._condition_name}" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id of the switch.""" return f"{self._server_unique_id}/{self._condition}" @property - def is_on(self): + def is_on(self) -> bool: """Return state of the switch.""" if self.api.data: return self.api.data[self._condition] - return None + return False async def async_turn_off(self, **kwargs): """Turn off the switch.""" From 5e059c7f55ebea6a77019570e60066d6c729d32c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Jul 2021 23:55:34 -0700 Subject: [PATCH 455/818] Fix lint on dev (#53265) --- homeassistant/components/mill/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 5b0cf7efd73..8b68d0ebe38 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -18,7 +18,7 @@ async def async_setup_entry(hass, entry, async_add_entities): dev = [] for heater in mill_data_connection.heaters.values(): - for sensor_type in [CONSUMPTION_TODAY, CONSUMPTION_YEAR]: + for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR): dev.append( MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) ) From 9b2d98f0277f31f71bdd50e27f91a916870f41a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 21 Jul 2021 08:56:29 +0200 Subject: [PATCH 456/818] Tibber, use nameclass (#53242) --- homeassistant/components/tibber/sensor.py | 137 ++++++++++++---------- 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 9ba175db57d..c48f4936200 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -2,11 +2,11 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass from datetime import timedelta from enum import Enum import logging from random import randrange +from typing import NamedTuple import aiohttp @@ -58,8 +58,7 @@ class ResetType(Enum): NEVER = "never" -@dataclass -class TibberSensorMetadata: +class TibberSensorMetadata(NamedTuple): """Metadata for an individual Tibber sensor.""" name: str @@ -71,119 +70,129 @@ class TibberSensorMetadata: RT_SENSOR_MAP: dict[str, TibberSensorMetadata] = { "averagePower": TibberSensorMetadata( - "average power", DEVICE_CLASS_POWER, POWER_WATT + "average power", + device_class=DEVICE_CLASS_POWER, + unit=POWER_WATT, ), "power": TibberSensorMetadata( "power", - DEVICE_CLASS_POWER, - POWER_WATT, + device_class=DEVICE_CLASS_POWER, + unit=POWER_WATT, ), "powerProduction": TibberSensorMetadata( - "power production", DEVICE_CLASS_POWER, POWER_WATT + "power production", + device_class=DEVICE_CLASS_POWER, + unit=POWER_WATT, + ), + "minPower": TibberSensorMetadata( + "min power", + device_class=DEVICE_CLASS_POWER, + unit=POWER_WATT, + ), + "maxPower": TibberSensorMetadata( + "max power", + device_class=DEVICE_CLASS_POWER, + unit=POWER_WATT, ), - "minPower": TibberSensorMetadata("min power", DEVICE_CLASS_POWER, POWER_WATT), - "maxPower": TibberSensorMetadata("max power", DEVICE_CLASS_POWER, POWER_WATT), "accumulatedConsumption": TibberSensorMetadata( "accumulated consumption", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - STATE_CLASS_MEASUREMENT, - ResetType.DAILY, + device_class=DEVICE_CLASS_ENERGY, + unit=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.DAILY, ), "accumulatedConsumptionLastHour": TibberSensorMetadata( "accumulated consumption current hour", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - STATE_CLASS_MEASUREMENT, - ResetType.HOURLY, + device_class=DEVICE_CLASS_ENERGY, + unit=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.HOURLY, ), "accumulatedProduction": TibberSensorMetadata( "accumulated production", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - STATE_CLASS_MEASUREMENT, - ResetType.DAILY, + device_class=DEVICE_CLASS_ENERGY, + unit=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.DAILY, ), "accumulatedProductionLastHour": TibberSensorMetadata( "accumulated production current hour", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - STATE_CLASS_MEASUREMENT, - ResetType.HOURLY, + device_class=DEVICE_CLASS_ENERGY, + unit=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.HOURLY, ), "lastMeterConsumption": TibberSensorMetadata( "last meter consumption", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + unit=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, ), "lastMeterProduction": TibberSensorMetadata( "last meter production", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + unit=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, ), "voltagePhase1": TibberSensorMetadata( "voltage phase1", - DEVICE_CLASS_VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + unit=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, ), "voltagePhase2": TibberSensorMetadata( "voltage phase2", - DEVICE_CLASS_VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + unit=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, ), "voltagePhase3": TibberSensorMetadata( "voltage phase3", - DEVICE_CLASS_VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + unit=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, ), "currentL1": TibberSensorMetadata( "current L1", - DEVICE_CLASS_CURRENT, - ELECTRIC_CURRENT_AMPERE, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_CURRENT, + unit=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, ), "currentL2": TibberSensorMetadata( "current L2", - DEVICE_CLASS_CURRENT, - ELECTRIC_CURRENT_AMPERE, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_CURRENT, + unit=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, ), "currentL3": TibberSensorMetadata( "current L3", - DEVICE_CLASS_CURRENT, - ELECTRIC_CURRENT_AMPERE, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_CURRENT, + unit=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, ), "signalStrength": TibberSensorMetadata( "signal strength", - DEVICE_CLASS_SIGNAL_STRENGTH, - SIGNAL_STRENGTH_DECIBELS, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit=SIGNAL_STRENGTH_DECIBELS, + state_class=STATE_CLASS_MEASUREMENT, ), "accumulatedReward": TibberSensorMetadata( "accumulated reward", - DEVICE_CLASS_MONETARY, - None, - STATE_CLASS_MEASUREMENT, - ResetType.DAILY, + device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.DAILY, ), "accumulatedCost": TibberSensorMetadata( "accumulated cost", - DEVICE_CLASS_MONETARY, - None, - STATE_CLASS_MEASUREMENT, - ResetType.DAILY, + device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.DAILY, ), "powerFactor": TibberSensorMetadata( "power factor", - DEVICE_CLASS_POWER_FACTOR, - PERCENTAGE, - STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_POWER_FACTOR, + unit=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), } @@ -362,7 +371,7 @@ class TibberSensorRT(TibberSensor): self._attr_state = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{metadata.name}" - if metadata.name in ["accumulated cost", "accumulated reward"]: + if metadata.name in ("accumulated cost", "accumulated reward"): self._attr_unit_of_measurement = tibber_home.currency else: self._attr_unit_of_measurement = metadata.unit From 4546e1467430588c50dc18d05a40425c2c7dee53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Jul 2021 10:02:07 +0200 Subject: [PATCH 457/818] Fix MQTT to allow setting an unknown Select state (#53227) --- homeassistant/components/mqtt/select.py | 5 ++++- tests/components/mqtt/test_select.py | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 98643917788..4f4d3fbb663 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -126,7 +126,10 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) - if payload not in self.options: + if payload.lower() == "none": + payload = None + + if payload is not None and payload not in self.options: _LOGGER.error( "Invalid option for %s: '%s' (valid options: %s)", self.entity_id, diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 5dad989a5cf..f2e48e10dc5 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -15,7 +15,7 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -122,6 +122,13 @@ async def test_value_template(hass, mqtt_mock): state = hass.states.get("select.test_select") assert state.state == "beer" + async_fire_mqtt_message(hass, topic, '{"val": null}') + + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == STATE_UNKNOWN + async def test_run_select_service_optimistic(hass, mqtt_mock): """Test that set_value service works in optimistic mode.""" From 18ec0544b9350cb2e77885e53cd05af9fb1931ea Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Wed, 21 Jul 2021 09:16:02 +0100 Subject: [PATCH 458/818] Allow for alternative external Growatt servers (#53102) --- .../components/growatt_server/config_flow.py | 11 ++++++++--- .../components/growatt_server/const.py | 8 ++++++++ .../components/growatt_server/sensor.py | 8 ++++++-- .../components/growatt_server/strings.json | 5 +++-- .../growatt_server/test_config_flow.py | 18 +++++++++++++----- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index cc1457d3687..45f56a327b2 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -3,10 +3,10 @@ import growattServer import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import callback -from .const import CONF_PLANT_ID, DOMAIN +from .const import CONF_PLANT_ID, DEFAULT_URL, DOMAIN, SERVER_URLS class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -24,7 +24,11 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _async_show_user_form(self, errors=None): """Show the form to the user.""" data_schema = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_URL, default=DEFAULT_URL): vol.In(SERVER_URLS), + } ) return self.async_show_form( @@ -36,6 +40,7 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return self._async_show_user_form() + self.api.server_url = user_input[CONF_URL] login_response = await self.hass.async_add_executor_job( self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] ) diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index 4dc09988e6f..0b11e9994ca 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -5,6 +5,14 @@ DEFAULT_PLANT_ID = "0" DEFAULT_NAME = "Growatt" +SERVER_URLS = [ + "https://server.growatt.com/", + "https://server-us.growatt.com", + "http://server.smten.com/", +] + +DEFAULT_URL = SERVER_URLS[0] + DOMAIN = "growatt_server" PLATFORMS = ["sensor"] diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 78fd24623d8..9d0aa098051 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, + CONF_URL, CONF_USERNAME, CURRENCY_EURO, DEVICE_CLASS_BATTERY, @@ -33,7 +34,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, dt -from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DOMAIN +from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DEFAULT_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -554,6 +555,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PLANT_ID, default=DEFAULT_PLANT_ID): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_URL, default=DEFAULT_URL): cv.string, } ) @@ -579,7 +581,7 @@ def get_device_list(api, config): # Log in to api and fetch first plant if no plant id is defined. login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD]) if not login_response["success"] and login_response["errCode"] == "102": - _LOGGER.error("Username or Password may be incorrect!") + _LOGGER.error("Username, Password or URL may be incorrect!") return user_id = login_response["userId"] if plant_id == DEFAULT_PLANT_ID: @@ -596,9 +598,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = config_entry.data username = config[CONF_USERNAME] password = config[CONF_PASSWORD] + url = config[CONF_URL] name = config[CONF_NAME] api = growattServer.GrowattApi() + api.server_url = url devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index e8d4f395c7b..45e25c0ba33 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -17,11 +17,12 @@ "data": { "name": "[%key:common::config_flow::data::name%]", "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "username": "[%key:common::config_flow::data::username%]", + "url": "[%key:common::config_flow::data::url%]" }, "title": "Enter your Growatt information" } } }, "title": "Growatt Server" -} +} \ No newline at end of file diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index cc11c2f8bf2..662448c8118 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -3,12 +3,20 @@ from copy import deepcopy from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.components.growatt_server.const import CONF_PLANT_ID, DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.growatt_server.const import ( + CONF_PLANT_ID, + DEFAULT_URL, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from tests.common import MockConfigEntry -FIXTURE_USER_INPUT = {CONF_USERNAME: "username", CONF_PASSWORD: "password"} +FIXTURE_USER_INPUT = { + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_URL: DEFAULT_URL, +} GROWATT_PLANT_LIST_RESPONSE = { "data": [ @@ -45,8 +53,8 @@ async def test_show_authenticate_form(hass): assert result["step_id"] == "user" -async def test_incorrect_username(hass): - """Test that it shows the appropriate error when an incorrect username is entered.""" +async def test_incorrect_login(hass): + """Test that it shows the appropriate error when an incorrect username/password/server is entered.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) From e9ce3c57cd18d42b234f9322c7e5bcefd1bc6e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 21 Jul 2021 10:25:46 +0200 Subject: [PATCH 459/818] Adax heaters (#50998) Co-authored-by: G Johansson <62932417+gjohansson-ST@users.noreply.github.com> Co-authored-by: Franck Nijhof --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/adax/__init__.py | 18 +++ homeassistant/components/adax/climate.py | 152 ++++++++++++++++++ homeassistant/components/adax/config_flow.py | 71 ++++++++ homeassistant/components/adax/const.py | 5 + homeassistant/components/adax/manifest.json | 13 ++ homeassistant/components/adax/strings.json | 20 +++ .../components/adax/translations/en.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/adax/__init__.py | 1 + tests/components/adax/test_config_flow.py | 78 +++++++++ 14 files changed, 389 insertions(+) create mode 100644 homeassistant/components/adax/__init__.py create mode 100644 homeassistant/components/adax/climate.py create mode 100644 homeassistant/components/adax/config_flow.py create mode 100644 homeassistant/components/adax/const.py create mode 100644 homeassistant/components/adax/manifest.json create mode 100644 homeassistant/components/adax/strings.json create mode 100644 homeassistant/components/adax/translations/en.json create mode 100644 tests/components/adax/__init__.py create mode 100644 tests/components/adax/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 84577e99197..49c2cb98424 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,8 @@ omit = homeassistant/components/acmeda/helpers.py homeassistant/components/acmeda/hub.py homeassistant/components/acmeda/sensor.py + homeassistant/components/adax/__init__.py + homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py homeassistant/components/adguard/const.py homeassistant/components/adguard/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index acd2b6d44c3..e5a9bcd5823 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -22,6 +22,7 @@ homeassistant/scripts/check_config.py @kellerza homeassistant/components/abode/* @shred86 homeassistant/components/accuweather/* @bieniu homeassistant/components/acmeda/* @atmurray +homeassistant/components/adax/* @danielhiversen homeassistant/components/adguard/* @frenck homeassistant/components/advantage_air/* @Bre77 homeassistant/components/aemet/* @noltari diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py new file mode 100644 index 00000000000..0a14648af26 --- /dev/null +++ b/homeassistant/components/adax/__init__.py @@ -0,0 +1,18 @@ +"""The Adax integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +PLATFORMS = ["climate"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Adax from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py new file mode 100644 index 00000000000..74e973ba6d5 --- /dev/null +++ b/homeassistant/components/adax/climate.py @@ -0,0 +1,152 @@ +"""Support for Adax wifi-enabled home heaters.""" +from __future__ import annotations + +import logging +from typing import Any + +from adax import Adax + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_PASSWORD, + PRECISION_WHOLE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ACCOUNT_ID + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Adax thermostat with config flow.""" + adax_data_handler = Adax( + entry.data[ACCOUNT_ID], + entry.data[CONF_PASSWORD], + websession=async_get_clientsession(hass), + ) + + async_add_entities( + AdaxDevice(room, adax_data_handler) + for room in await adax_data_handler.get_rooms() + ) + + +class AdaxDevice(ClimateEntity): + """Representation of a heater.""" + + def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: + """Initialize the heater.""" + self._heater_data = heater_data + self._adax_data_handler = adax_data_handler + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._heater_data['homeId']}_{self._heater_data['id']}" + + @property + def name(self) -> str: + """Return the name of the device, if any.""" + return self._heater_data["name"] + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + if self._heater_data["heatingEnabled"]: + return HVAC_MODE_HEAT + return HVAC_MODE_OFF + + @property + def icon(self) -> str: + """Return nice icon for heater.""" + if self.hvac_mode == HVAC_MODE_HEAT: + return "mdi:radiator" + return "mdi:radiator-off" + + @property + def hvac_modes(self) -> list[str]: + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_HEAT, HVAC_MODE_OFF] + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set hvac mode.""" + if hvac_mode == HVAC_MODE_HEAT: + temperature = max( + self.min_temp, self._heater_data.get("targetTemperature", self.min_temp) + ) + await self._adax_data_handler.set_room_target_temperature( + self._heater_data["id"], temperature, True + ) + elif hvac_mode == HVAC_MODE_OFF: + await self._adax_data_handler.set_room_target_temperature( + self._heater_data["id"], self.min_temp, False + ) + else: + return + await self._adax_data_handler.update() + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement which this device uses.""" + return TEMP_CELSIUS + + @property + def min_temp(self) -> int: + """Return the minimum temperature.""" + return 5 + + @property + def max_temp(self) -> int: + """Return the maximum temperature.""" + return 35 + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._heater_data.get("temperature") + + @property + def target_temperature(self) -> int | None: + """Return the temperature we try to reach.""" + return self._heater_data.get("targetTemperature") + + @property + def target_temperature_step(self) -> int: + """Return the supported step of target temperature.""" + return PRECISION_WHOLE + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._adax_data_handler.set_room_target_temperature( + self._heater_data["id"], temperature, True + ) + + async def async_update(self) -> None: + """Get the latest data.""" + for room in await self._adax_data_handler.get_rooms(): + if room["id"] == self._heater_data["id"]: + self._heater_data = room + return diff --git a/homeassistant/components/adax/config_flow.py b/homeassistant/components/adax/config_flow.py new file mode 100644 index 00000000000..166278ef48d --- /dev/null +++ b/homeassistant/components/adax/config_flow.py @@ -0,0 +1,71 @@ +"""Config flow for Adax integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import adax +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ACCOUNT_ID, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + {vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str} +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Validate the user input allows us to connect.""" + account_id = data[ACCOUNT_ID] + password = data[CONF_PASSWORD].replace(" ", "") + + token = await adax.get_adax_token( + async_get_clientsession(hass), account_id, password + ) + if token is None: + _LOGGER.info("Adax: Failed to login to retrieve token") + raise CannotConnect + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Adax.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + self._async_abort_entries_match({ACCOUNT_ID: user_input[ACCOUNT_ID]}) + await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[ACCOUNT_ID], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/adax/const.py b/homeassistant/components/adax/const.py new file mode 100644 index 00000000000..ecb83f9b0f7 --- /dev/null +++ b/homeassistant/components/adax/const.py @@ -0,0 +1,5 @@ +"""Constants for the Adax integration.""" +from typing import Final + +ACCOUNT_ID: Final = "account_id" +DOMAIN: Final = "adax" diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json new file mode 100644 index 00000000000..36106290ed6 --- /dev/null +++ b/homeassistant/components/adax/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "adax", + "name": "Adax", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/adax", + "requirements": [ + "adax==0.0.1" + ], + "codeowners": [ + "@danielhiversen" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/adax/strings.json b/homeassistant/components/adax/strings.json new file mode 100644 index 00000000000..0f7aac83f5a --- /dev/null +++ b/homeassistant/components/adax/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "account_id": "Account ID", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/adax/translations/en.json b/homeassistant/components/adax/translations/en.json new file mode 100644 index 00000000000..a5a204c93f8 --- /dev/null +++ b/homeassistant/components/adax/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "account_id": "Account ID" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 45a339ebed1..1321c01b27d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -9,6 +9,7 @@ FLOWS = [ "abode", "accuweather", "acmeda", + "adax", "adguard", "advantage_air", "aemet", diff --git a/requirements_all.txt b/requirements_all.txt index a0b2fa1d554..0559e927aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -104,6 +104,9 @@ adafruit-circuitpython-dht==3.6.0 # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==2.2.2 +# homeassistant.components.adax +adax==0.0.1 + # homeassistant.components.androidtv adb-shell[async]==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36cfed2598e..80f25c59e31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -47,6 +47,9 @@ abodepy==1.2.0 # homeassistant.components.accuweather accuweather==0.2.0 +# homeassistant.components.adax +adax==0.0.1 + # homeassistant.components.androidtv adb-shell[async]==0.3.4 diff --git a/tests/components/adax/__init__.py b/tests/components/adax/__init__.py new file mode 100644 index 00000000000..54a72856a85 --- /dev/null +++ b/tests/components/adax/__init__.py @@ -0,0 +1 @@ +"""Tests for the Adax integration.""" diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py new file mode 100644 index 00000000000..f9638e52cbf --- /dev/null +++ b/tests/components/adax/test_config_flow.py @@ -0,0 +1,78 @@ +"""Test the Adax config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.adax.const import ACCOUNT_ID, DOMAIN +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +TEST_DATA = { + ACCOUNT_ID: 12345, + CONF_PASSWORD: "pswd", +} + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch("adax.get_adax_token", return_value="test_token",), patch( + "homeassistant.components.adax.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_DATA["account_id"] + assert result2["data"] == { + "account_id": TEST_DATA["account_id"], + "password": TEST_DATA["password"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "adax.get_adax_token", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: + """Test user input for config_entry that already exists.""" + + first_entry = MockConfigEntry( + domain="adax", + data=TEST_DATA, + unique_id=TEST_DATA[ACCOUNT_ID], + ) + first_entry.add_to_hass(hass) + + with patch("adax.get_adax_token", return_value="token"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TEST_DATA + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From 81c4d95afe9577001f4f12ef2b5a5d3ccf4dc396 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 05:31:50 -0400 Subject: [PATCH 460/818] Use entity class attributes for arduino (#52677) * Use entity class attributes for arduino * Revert state * tweak * tweak --- homeassistant/components/arduino/sensor.py | 19 +++-------------- homeassistant/components/arduino/switch.py | 24 ++++++---------------- 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index 588a652660a..fa624a7d167 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -35,24 +35,11 @@ class ArduinoSensor(SensorEntity): def __init__(self, name, pin, pin_type, board): """Initialize the sensor.""" self._pin = pin - self._name = name - self.pin_type = pin_type - self.direction = "in" - self._value = None + self._attr_name = name - board.set_mode(self._pin, self.direction, self.pin_type) + board.set_mode(self._pin, "in", pin_type) self._board = board - @property - def state(self): - """Return the state of the sensor.""" - return self._value - - @property - def name(self): - """Get the name of the sensor.""" - return self._name - def update(self): """Get the latest value from the pin.""" - self._value = self._board.get_analog_inputs()[self._pin][1] + self._attr_state = self._board.get_analog_inputs()[self._pin][1] diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py index 6ee742fd506..9368426b38e 100644 --- a/homeassistant/components/arduino/switch.py +++ b/homeassistant/components/arduino/switch.py @@ -43,11 +43,9 @@ class ArduinoSwitch(SwitchEntity): def __init__(self, pin, options, board): """Initialize the Pin.""" self._pin = pin - self._name = options[CONF_NAME] - self.pin_type = CONF_TYPE - self.direction = "out" + self._attr_name = options[CONF_NAME] - self._state = options[CONF_INITIAL] + self._attr_is_on = options[CONF_INITIAL] if options[CONF_NEGATE]: self.turn_on_handler = board.set_digital_out_low @@ -56,25 +54,15 @@ class ArduinoSwitch(SwitchEntity): self.turn_on_handler = board.set_digital_out_high self.turn_off_handler = board.set_digital_out_low - board.set_mode(self._pin, self.direction, self.pin_type) - (self.turn_on_handler if self._state else self.turn_off_handler)(pin) - - @property - def name(self): - """Get the name of the pin.""" - return self._name - - @property - def is_on(self): - """Return true if pin is high/on.""" - return self._state + board.set_mode(pin, "out", CONF_TYPE) + (self.turn_on_handler if self.is_on else self.turn_off_handler)(pin) def turn_on(self, **kwargs): """Turn the pin to high/on.""" - self._state = True + self._attr_is_on = True self.turn_on_handler(self._pin) def turn_off(self, **kwargs): """Turn the pin to low/off.""" - self._state = False + self._attr_is_on = False self.turn_off_handler(self._pin) From 02a7a2464a60c2d44dc6a8e66ca36579e8ddb93f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 05:33:44 -0400 Subject: [PATCH 461/818] Use entity class attributes for atag (#52686) --- homeassistant/components/atag/__init__.py | 19 ++-------- homeassistant/components/atag/climate.py | 27 ++++--------- homeassistant/components/atag/sensor.py | 38 +++++++------------ homeassistant/components/atag/water_heater.py | 17 ++------- 4 files changed, 29 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index af5eff67f57..de785a3a317 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -75,27 +75,16 @@ class AtagEntity(CoordinatorEntity): super().__init__(coordinator) self._id = atag_id - self._name = DOMAIN.title() + self._attr_name = DOMAIN.title() + self._attr_unique_id = f"{coordinator.data.id}-{atag_id}" @property def device_info(self) -> DeviceInfo: """Return info for device registry.""" - device = self.coordinator.data.id - version = self.coordinator.data.apiversion return { - "identifiers": {(DOMAIN, device)}, + "identifiers": {(DOMAIN, self.coordinator.data.id)}, "name": "Atag Thermostat", "model": "Atag One", - "sw_version": version, + "sw_version": self.coordinator.data.apiversion, "manufacturer": "Atag", } - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID to use for this entity.""" - return f"{self.coordinator.data.id}-{self._id}" diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index da7e6a14a73..6bafd59ab82 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -37,10 +37,14 @@ async def async_setup_entry(hass, entry, async_add_entities): class AtagThermostat(AtagEntity, ClimateEntity): """Atag climate device.""" - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS + _attr_hvac_modes = HVAC_MODES + _attr_preset_modes = list(PRESET_MAP.keys()) + _attr_supported_features = SUPPORT_FLAGS + + def __init__(self, coordinator, atag_id): + """Initialize an Atag climate device.""" + super().__init__(coordinator, atag_id) + self._attr_temperature_unit = coordinator.data.climate.temp_unit @property def hvac_mode(self) -> str | None: @@ -49,22 +53,12 @@ class AtagThermostat(AtagEntity, ClimateEntity): return self.coordinator.data.climate.hvac_mode return None - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return HVAC_MODES - @property def hvac_action(self) -> str | None: """Return the current running hvac operation.""" is_active = self.coordinator.data.climate.status return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE - @property - def temperature_unit(self) -> str | None: - """Return the unit of measurement.""" - return self.coordinator.data.climate.temp_unit - @property def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -81,11 +75,6 @@ class AtagThermostat(AtagEntity, ClimateEntity): preset = self.coordinator.data.climate.preset_mode return PRESET_INVERTED.get(preset) - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return list(PRESET_MAP.keys()) - async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 88ccbdc899f..93164bd14bf 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -36,7 +36,20 @@ class AtagSensor(AtagEntity, SensorEntity): def __init__(self, coordinator, sensor): """Initialize Atag sensor.""" super().__init__(coordinator, SENSORS[sensor]) - self._name = sensor + self._attr_name = sensor + if coordinator.data.report[self._id].sensorclass in [ + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ]: + self._attr_device_class = coordinator.data.report[self._id].sensorclass + if coordinator.data.report[self._id].measure in [ + PRESSURE_BAR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + PERCENTAGE, + TIME_HOURS, + ]: + self._attr_unit_of_measurement = coordinator.data.report[self._id].measure @property def state(self): @@ -47,26 +60,3 @@ class AtagSensor(AtagEntity, SensorEntity): def icon(self): """Return icon.""" return self.coordinator.data.report[self._id].icon - - @property - def device_class(self): - """Return deviceclass.""" - if self.coordinator.data.report[self._id].sensorclass in [ - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - ]: - return self.coordinator.data.report[self._id].sensorclass - return None - - @property - def unit_of_measurement(self): - """Return measure.""" - if self.coordinator.data.report[self._id].measure in [ - PRESSURE_BAR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - PERCENTAGE, - TIME_HOURS, - ]: - return self.coordinator.data.report[self._id].measure - return None diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index dac56edf89d..5fce2abf63e 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -22,15 +22,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AtagWaterHeater(AtagEntity, WaterHeaterEntity): """Representation of an ATAG water heater.""" - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_HEATER - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS + _attr_operation_list = OPERATION_LIST + _attr_supported_features = SUPPORT_FLAGS_HEATER + _attr_temperature_unit = TEMP_CELSIUS @property def current_temperature(self): @@ -43,11 +37,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): operation = self.coordinator.data.dhw.current_operation return operation if operation in self.operation_list else STATE_OFF - @property - def operation_list(self): - """List of available operation modes.""" - return OPERATION_LIST - async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): From 462db1b4b2794e09d38d42093d2a0c33e805e98b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 07:31:54 -0400 Subject: [PATCH 462/818] Add config flow to nfandroidtv (#51280) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/nfandroidtv/__init__.py | 70 +++- .../components/nfandroidtv/config_flow.py | 76 +++++ homeassistant/components/nfandroidtv/const.py | 28 ++ .../components/nfandroidtv/manifest.json | 6 +- .../components/nfandroidtv/notify.py | 300 +++++++----------- .../components/nfandroidtv/strings.json | 21 ++ .../nfandroidtv/translations/en.json | 21 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nfandroidtv/__init__.py | 31 ++ .../nfandroidtv/test_config_flow.py | 135 ++++++++ 14 files changed, 512 insertions(+), 185 deletions(-) create mode 100644 homeassistant/components/nfandroidtv/config_flow.py create mode 100644 homeassistant/components/nfandroidtv/const.py create mode 100644 homeassistant/components/nfandroidtv/strings.json create mode 100644 homeassistant/components/nfandroidtv/translations/en.json create mode 100644 tests/components/nfandroidtv/__init__.py create mode 100644 tests/components/nfandroidtv/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 49c2cb98424..83212125cb7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -693,6 +693,7 @@ omit = homeassistant/components/neurio_energy/sensor.py homeassistant/components/nexia/climate.py homeassistant/components/nextcloud/* + homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py diff --git a/CODEOWNERS b/CODEOWNERS index e5a9bcd5823..5fa884fda15 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ homeassistant/components/netdata/* @fabaff homeassistant/components/nexia/* @bdraco homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextcloud/* @meichthys +homeassistant/components/nfandroidtv/* @tkdrob homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 9965265e00d..90a76c1c747 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -1 +1,69 @@ -"""The nfandroidtv component.""" +"""The NFAndroidTV integration.""" +import logging + +from notifications_android_tv.notifications import ConnectError, Notifications + +from homeassistant.components.notify import DOMAIN as NOTIFY +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [NOTIFY] + + +async def async_setup(hass: HomeAssistant, config): + """Set up the NFAndroidTV component.""" + hass.data.setdefault(DOMAIN, {}) + # Iterate all entries for notify to only get nfandroidtv + if NOTIFY in config: + for entry in config[NOTIFY]: + if entry[CONF_PLATFORM] == DOMAIN: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up NFAndroidTV from a config entry.""" + host = entry.data[CONF_HOST] + name = entry.data[CONF_NAME] + + try: + await hass.async_add_executor_job(Notifications, host) + except ConnectError as ex: + _LOGGER.warning("Failed to connect: %s", ex) + raise ConfigEntryNotReady from ex + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + CONF_HOST: host, + CONF_NAME: name, + } + + hass.async_create_task( + discovery.async_load_platform( + hass, NOTIFY, DOMAIN, hass.data[DOMAIN][entry.entry_id], hass.data[DOMAIN] + ) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nfandroidtv/config_flow.py b/homeassistant/components/nfandroidtv/config_flow.py new file mode 100644 index 00000000000..0f7cffcff4b --- /dev/null +++ b/homeassistant/components/nfandroidtv/config_flow.py @@ -0,0 +1,76 @@ +"""Config flow for NFAndroidTV integration.""" +from __future__ import annotations + +import logging + +from notifications_android_tv.notifications import ConnectError, Notifications +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NFAndroidTV.""" + + async def async_step_user(self, user_input=None) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + host = user_input[CONF_HOST] + name = user_input[CONF_NAME] + + await self.async_set_unique_id(host) + self._abort_if_unique_id_configured() + error = await self._async_try_connect(host) + if error is None: + return self.async_create_entry( + title=name, + data={CONF_HOST: host, CONF_NAME: name}, + ) + errors["base"] = error + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + } + ), + errors=errors, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == import_config[CONF_HOST]: + _LOGGER.warning( + "Already configured. This yaml configuration has already been imported. Please remove it" + ) + return self.async_abort(reason="already_configured") + if CONF_NAME not in import_config: + import_config[CONF_NAME] = f"{DEFAULT_NAME} {import_config[CONF_HOST]}" + + return await self.async_step_user(import_config) + + async def _async_try_connect(self, host): + """Try connecting to Android TV / Fire TV.""" + try: + await self.hass.async_add_executor_job(Notifications, host) + except ConnectError: + _LOGGER.error("Error connecting to device at %s", host) + return "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return "unknown" + return diff --git a/homeassistant/components/nfandroidtv/const.py b/homeassistant/components/nfandroidtv/const.py new file mode 100644 index 00000000000..332c1754771 --- /dev/null +++ b/homeassistant/components/nfandroidtv/const.py @@ -0,0 +1,28 @@ +"""Constants for the NFAndroidTV integration.""" +DOMAIN: str = "nfandroidtv" +CONF_DURATION = "duration" +CONF_FONTSIZE = "fontsize" +CONF_POSITION = "position" +CONF_TRANSPARENCY = "transparency" +CONF_COLOR = "color" +CONF_INTERRUPT = "interrupt" + +DEFAULT_NAME = "Android TV / Fire TV" +DEFAULT_TIMEOUT = 5 + +ATTR_DURATION = "duration" +ATTR_FONTSIZE = "fontsize" +ATTR_POSITION = "position" +ATTR_TRANSPARENCY = "transparency" +ATTR_COLOR = "color" +ATTR_BKGCOLOR = "bkgcolor" +ATTR_INTERRUPT = "interrupt" +ATTR_FILE = "file" +# Attributes contained in file +ATTR_FILE_URL = "url" +ATTR_FILE_PATH = "path" +ATTR_FILE_USERNAME = "username" +ATTR_FILE_PASSWORD = "password" +ATTR_FILE_AUTH = "auth" +# Any other value or absence of 'auth' lead to basic authentication being used +ATTR_FILE_AUTH_DIGEST = "digest" diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 6f29d4d410e..5516f144fd4 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -1,7 +1,9 @@ { "domain": "nfandroidtv", - "name": "Notifications for Android TV / FireTV", + "name": "Notifications for Android TV / Fire TV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "codeowners": [], + "requirements": ["notifications-android-tv==0.1.2"], + "codeowners": ["@tkdrob"], + "config_flow": true, "iot_class": "local_push" } diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index ad2f3fb3706..c2a42760aec 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -1,8 +1,7 @@ """Notifications for Android TV notification service.""" -import base64 -import io import logging +from notifications_android_tv import Notifications import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol @@ -14,115 +13,69 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_HOST, CONF_TIMEOUT, HTTP_OK, PERCENTAGE +from homeassistant.const import ATTR_ICON, CONF_HOST, CONF_TIMEOUT +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from .const import ( + ATTR_COLOR, + ATTR_DURATION, + ATTR_FILE, + ATTR_FILE_AUTH, + ATTR_FILE_AUTH_DIGEST, + ATTR_FILE_PASSWORD, + ATTR_FILE_PATH, + ATTR_FILE_URL, + ATTR_FILE_USERNAME, + ATTR_FONTSIZE, + ATTR_INTERRUPT, + ATTR_POSITION, + ATTR_TRANSPARENCY, + CONF_COLOR, + CONF_DURATION, + CONF_FONTSIZE, + CONF_INTERRUPT, + CONF_POSITION, + CONF_TRANSPARENCY, + DEFAULT_TIMEOUT, +) + _LOGGER = logging.getLogger(__name__) -CONF_DURATION = "duration" -CONF_FONTSIZE = "fontsize" -CONF_POSITION = "position" -CONF_TRANSPARENCY = "transparency" -CONF_COLOR = "color" -CONF_INTERRUPT = "interrupt" - -DEFAULT_DURATION = 5 -DEFAULT_FONTSIZE = "medium" -DEFAULT_POSITION = "bottom-right" -DEFAULT_TRANSPARENCY = "default" -DEFAULT_COLOR = "grey" -DEFAULT_INTERRUPT = False -DEFAULT_TIMEOUT = 5 -DEFAULT_ICON = ( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApo" - "cMXEAAAAASUVORK5CYII=" -) - -ATTR_DURATION = "duration" -ATTR_FONTSIZE = "fontsize" -ATTR_POSITION = "position" -ATTR_TRANSPARENCY = "transparency" -ATTR_COLOR = "color" -ATTR_BKGCOLOR = "bkgcolor" -ATTR_INTERRUPT = "interrupt" -ATTR_IMAGE = "filename2" -ATTR_FILE = "file" -# Attributes contained in file -ATTR_FILE_URL = "url" -ATTR_FILE_PATH = "path" -ATTR_FILE_USERNAME = "username" -ATTR_FILE_PASSWORD = "password" -ATTR_FILE_AUTH = "auth" -# Any other value or absence of 'auth' lead to basic authentication being used -ATTR_FILE_AUTH_DIGEST = "digest" - -FONTSIZES = {"small": 1, "medium": 0, "large": 2, "max": 3} - -POSITIONS = { - "bottom-right": 0, - "bottom-left": 1, - "top-right": 2, - "top-left": 3, - "center": 4, -} - -TRANSPARENCIES = { - "default": 0, - f"0{PERCENTAGE}": 1, - f"25{PERCENTAGE}": 2, - f"50{PERCENTAGE}": 3, - f"75{PERCENTAGE}": 4, - f"100{PERCENTAGE}": 5, -} - -COLORS = { - "grey": "#607d8b", - "black": "#000000", - "indigo": "#303F9F", - "green": "#4CAF50", - "red": "#F44336", - "cyan": "#00BCD4", - "teal": "#009688", - "amber": "#FFC107", - "pink": "#E91E63", -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Coerce(int), - vol.Optional(CONF_FONTSIZE, default=DEFAULT_FONTSIZE): vol.In(FONTSIZES.keys()), - vol.Optional(CONF_POSITION, default=DEFAULT_POSITION): vol.In(POSITIONS.keys()), - vol.Optional(CONF_TRANSPARENCY, default=DEFAULT_TRANSPARENCY): vol.In( - TRANSPARENCIES.keys() +# Deprecated in Home Assistant 2021.8 +PLATFORM_SCHEMA = cv.deprecated( + vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DURATION): vol.Coerce(int), + vol.Optional(CONF_FONTSIZE): vol.In(Notifications.FONTSIZES.keys()), + vol.Optional(CONF_POSITION): vol.In(Notifications.POSITIONS.keys()), + vol.Optional(CONF_TRANSPARENCY): vol.In( + Notifications.TRANSPARENCIES.keys() + ), + vol.Optional(CONF_COLOR): vol.In(Notifications.BKG_COLORS.keys()), + vol.Optional(CONF_TIMEOUT): vol.Coerce(int), + vol.Optional(CONF_INTERRUPT): cv.boolean, + } ), - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(COLORS.keys()), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), - vol.Optional(CONF_INTERRUPT, default=DEFAULT_INTERRUPT): cv.boolean, - } + ) ) -def get_service(hass, config, discovery_info=None): - """Get the Notifications for Android TV notification service.""" - remoteip = config.get(CONF_HOST) - duration = config.get(CONF_DURATION) - fontsize = config.get(CONF_FONTSIZE) - position = config.get(CONF_POSITION) - transparency = config.get(CONF_TRANSPARENCY) - color = config.get(CONF_COLOR) - interrupt = config.get(CONF_INTERRUPT) - timeout = config.get(CONF_TIMEOUT) - +async def async_get_service(hass: HomeAssistant, config, discovery_info=None): + """Get the NFAndroidTV notification service.""" + if discovery_info is not None: + notify = await hass.async_add_executor_job( + Notifications, discovery_info[CONF_HOST] + ) + return NFAndroidTVNotificationService( + notify, + hass.config.is_allowed_path, + ) + notify = await hass.async_add_executor_job(Notifications, config.get(CONF_HOST)) return NFAndroidTVNotificationService( - remoteip, - duration, - fontsize, - position, - transparency, - color, - interrupt, - timeout, + notify, hass.config.is_allowed_path, ) @@ -132,116 +85,98 @@ class NFAndroidTVNotificationService(BaseNotificationService): def __init__( self, - remoteip, - duration, - fontsize, - position, - transparency, - color, - interrupt, - timeout, + notify: Notifications, is_allowed_path, ): """Initialize the service.""" - self._target = f"http://{remoteip}:7676" - self._default_duration = duration - self._default_fontsize = fontsize - self._default_position = position - self._default_transparency = transparency - self._default_color = color - self._default_interrupt = interrupt - self._timeout = timeout - self._icon_file = io.BytesIO(base64.b64decode(DEFAULT_ICON)) + self.notify = notify self.is_allowed_path = is_allowed_path def send_message(self, message="", **kwargs): """Send a message to a Android TV device.""" - _LOGGER.debug("Sending notification to: %s", self._target) - - payload = { - "filename": ( - "icon.png", - self._icon_file, - "application/octet-stream", - {"Expires": "0"}, - ), - "type": "0", - "title": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - "msg": message, - "duration": "%i" % self._default_duration, - "fontsize": "%i" % FONTSIZES.get(self._default_fontsize), - "position": "%i" % POSITIONS.get(self._default_position), - "bkgcolor": "%s" % COLORS.get(self._default_color), - "transparency": "%i" % TRANSPARENCIES.get(self._default_transparency), - "offset": "0", - "app": ATTR_TITLE_DEFAULT, - "force": "true", - "interrupt": "%i" % self._default_interrupt, - } - data = kwargs.get(ATTR_DATA) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + duration = None + fontsize = None + position = None + transparency = None + bkgcolor = None + interrupt = None + icon = None + image_file = None if data: if ATTR_DURATION in data: - duration = data.get(ATTR_DURATION) try: - payload[ATTR_DURATION] = "%i" % int(duration) + duration = int(data.get(ATTR_DURATION)) except ValueError: - _LOGGER.warning("Invalid duration-value: %s", str(duration)) + _LOGGER.warning( + "Invalid duration-value: %s", str(data.get(ATTR_DURATION)) + ) if ATTR_FONTSIZE in data: - fontsize = data.get(ATTR_FONTSIZE) - if fontsize in FONTSIZES: - payload[ATTR_FONTSIZE] = "%i" % FONTSIZES.get(fontsize) + if data.get(ATTR_FONTSIZE) in Notifications.FONTSIZES: + fontsize = data.get(ATTR_FONTSIZE) else: - _LOGGER.warning("Invalid fontsize-value: %s", str(fontsize)) + _LOGGER.warning( + "Invalid fontsize-value: %s", str(data.get(ATTR_FONTSIZE)) + ) if ATTR_POSITION in data: - position = data.get(ATTR_POSITION) - if position in POSITIONS: - payload[ATTR_POSITION] = "%i" % POSITIONS.get(position) + if data.get(ATTR_POSITION) in Notifications.POSITIONS: + position = data.get(ATTR_POSITION) else: - _LOGGER.warning("Invalid position-value: %s", str(position)) + _LOGGER.warning( + "Invalid position-value: %s", str(data.get(ATTR_POSITION)) + ) if ATTR_TRANSPARENCY in data: - transparency = data.get(ATTR_TRANSPARENCY) - if transparency in TRANSPARENCIES: - payload[ATTR_TRANSPARENCY] = "%i" % TRANSPARENCIES.get(transparency) + if data.get(ATTR_TRANSPARENCY) in Notifications.TRANSPARENCIES: + transparency = data.get(ATTR_TRANSPARENCY) else: - _LOGGER.warning("Invalid transparency-value: %s", str(transparency)) + _LOGGER.warning( + "Invalid transparency-value: %s", + str(data.get(ATTR_TRANSPARENCY)), + ) if ATTR_COLOR in data: - color = data.get(ATTR_COLOR) - if color in COLORS: - payload[ATTR_BKGCOLOR] = "%s" % COLORS.get(color) + if data.get(ATTR_COLOR) in Notifications.BKG_COLORS: + bkgcolor = data.get(ATTR_COLOR) else: - _LOGGER.warning("Invalid color-value: %s", str(color)) + _LOGGER.warning( + "Invalid color-value: %s", str(data.get(ATTR_COLOR)) + ) if ATTR_INTERRUPT in data: - interrupt = data.get(ATTR_INTERRUPT) try: - payload[ATTR_INTERRUPT] = "%i" % cv.boolean(interrupt) + interrupt = cv.boolean(data.get(ATTR_INTERRUPT)) except vol.Invalid: - _LOGGER.warning("Invalid interrupt-value: %s", str(interrupt)) + _LOGGER.warning( + "Invalid interrupt-value: %s", str(data.get(ATTR_INTERRUPT)) + ) filedata = data.get(ATTR_FILE) if data else None if filedata is not None: - # Load from file or URL - file_as_bytes = self.load_file( + if ATTR_ICON in filedata: + icon = self.load_file( + url=filedata.get(ATTR_ICON), + local_path=filedata.get(ATTR_FILE_PATH), + username=filedata.get(ATTR_FILE_USERNAME), + password=filedata.get(ATTR_FILE_PASSWORD), + auth=filedata.get(ATTR_FILE_AUTH), + ) + image_file = self.load_file( url=filedata.get(ATTR_FILE_URL), local_path=filedata.get(ATTR_FILE_PATH), username=filedata.get(ATTR_FILE_USERNAME), password=filedata.get(ATTR_FILE_PASSWORD), auth=filedata.get(ATTR_FILE_AUTH), ) - if file_as_bytes: - payload[ATTR_IMAGE] = ( - "image", - file_as_bytes, - "application/octet-stream", - {"Expires": "0"}, - ) - - try: - _LOGGER.debug("Payload: %s", str(payload)) - response = requests.post(self._target, files=payload, timeout=self._timeout) - if response.status_code != HTTP_OK: - _LOGGER.error("Error sending message: %s", str(response)) - except requests.exceptions.ConnectionError as err: - _LOGGER.error("Error communicating with %s: %s", self._target, str(err)) + self.notify.send( + message, + title=title, + duration=duration, + fontsize=fontsize, + position=position, + bkgcolor=bkgcolor, + transparency=transparency, + interrupt=interrupt, + icon=icon, + image_file=image_file, + ) def load_file( self, url=None, local_path=None, username=None, password=None, auth=None @@ -266,7 +201,8 @@ class NFAndroidTVNotificationService(BaseNotificationService): if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - return open(local_path, "rb") # pylint: disable=consider-using-with + with open(local_path, "rb") as path_handle: + return path_handle _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: _LOGGER.warning("Neither URL nor local path found in params!") diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json new file mode 100644 index 00000000000..5940f86a406 --- /dev/null +++ b/homeassistant/components/nfandroidtv/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "Notifications for Android TV / Fire TV", + "description": "This integration requires the Notifications for Android TV app.\n\nFor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nYou should set up either DHCP reservation on your router (refer to your router's user manual) or a static IP address on the device. If not, the device will eventually become unavailable.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "[%key:common::config_flow::data::name%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/nfandroidtv/translations/en.json b/homeassistant/components/nfandroidtv/translations/en.json new file mode 100644 index 00000000000..22d014c1ffa --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + }, + "description": "This integration requires the Notifications for Android TV app.\n\nFor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nYou should set up either DHCP reservation on your router (refer to your router's user manual) or a static IP address on the device. If not, the device will eventually become unavailable.", + "title": "Notifications for Android TV / Fire TV" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1321c01b27d..943ca9cda74 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -178,6 +178,7 @@ FLOWS = [ "nest", "netatmo", "nexia", + "nfandroidtv", "nightscout", "notion", "nuheat", diff --git a/requirements_all.txt b/requirements_all.txt index 0559e927aec..bb119c6a2c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1032,6 +1032,9 @@ niluclient==0.1.2 # homeassistant.components.noaa_tides noaa-coops==0.1.8 +# homeassistant.components.nfandroidtv +notifications-android-tv==0.1.2 + # homeassistant.components.notify_events notify-events==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80f25c59e31..22a017442eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -577,6 +577,9 @@ nettigo-air-monitor==1.0.0 # homeassistant.components.nexia nexia==0.9.10 +# homeassistant.components.nfandroidtv +notifications-android-tv==0.1.2 + # homeassistant.components.notify_events notify-events==1.0.4 diff --git a/tests/components/nfandroidtv/__init__.py b/tests/components/nfandroidtv/__init__.py new file mode 100644 index 00000000000..056e2b2bc71 --- /dev/null +++ b/tests/components/nfandroidtv/__init__.py @@ -0,0 +1,31 @@ +"""Tests for the NFAndroidTV integration.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant.const import CONF_HOST, CONF_NAME + +HOST = "1.2.3.4" +NAME = "Android TV / Fire TV" + +CONF_DATA = { + CONF_HOST: HOST, + CONF_NAME: NAME, +} + +CONF_CONFIG_FLOW = { + CONF_HOST: HOST, + CONF_NAME: NAME, +} + + +async def _create_mocked_tv(raise_exception=False): + mocked_tv = AsyncMock() + mocked_tv.get_state = AsyncMock() + return mocked_tv + + +def _patch_config_flow_tv(mocked_tv): + return patch( + "homeassistant.components.nfandroidtv.config_flow.Notifications", + return_value=mocked_tv, + ) diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py new file mode 100644 index 00000000000..b16b053c70f --- /dev/null +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test NFAndroidTV config flow.""" +from unittest.mock import patch + +from notifications_android_tv.notifications import ConnectError + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.nfandroidtv.const import DEFAULT_NAME, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME + +from . import ( + CONF_CONFIG_FLOW, + CONF_DATA, + HOST, + NAME, + _create_mocked_tv, + _patch_config_flow_tv, +) + +from tests.common import MockConfigEntry + + +def _patch_setup(): + return patch( + "homeassistant.components.nfandroidtv.async_setup_entry", + return_value=True, + ) + + +async def test_flow_user(hass): + """Test user initialized flow.""" + mocked_tv = await _create_mocked_tv() + with _patch_config_flow_tv(mocked_tv), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured(hass): + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_CONFIG_FLOW, + unique_id=HOST, + ) + + entry.add_to_hass(hass) + + mocked_tv = await _create_mocked_tv() + with _patch_config_flow_tv(mocked_tv), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect(hass): + """Test user initialized flow with unreachable server.""" + mocked_tv = await _create_mocked_tv(True) + with _patch_config_flow_tv(mocked_tv) as tvmock: + tvmock.side_effect = ConnectError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_CONFIG_FLOW, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_user_unknown_error(hass): + """Test user initialized flow with unreachable server.""" + mocked_tv = await _create_mocked_tv(True) + with _patch_config_flow_tv(mocked_tv) as tvmock: + tvmock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_CONFIG_FLOW, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_import(hass): + """Test an import flow.""" + mocked_tv = await _create_mocked_tv(True) + with _patch_config_flow_tv(mocked_tv), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_CONFIG_FLOW, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == CONF_DATA + + with _patch_config_flow_tv(mocked_tv), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_CONFIG_FLOW, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_import_missing_optional(hass): + """Test an import flow with missing options.""" + mocked_tv = await _create_mocked_tv(True) + with _patch_config_flow_tv(mocked_tv), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: HOST}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_HOST: HOST, CONF_NAME: f"{DEFAULT_NAME} {HOST}"} From 7fef87691af5c4231bc37e5d935a7d9ac7fdd8a8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 08:07:26 -0400 Subject: [PATCH 463/818] Use entity class attributes for airvisual (#52503) * Use entity class attributes for airvisual * fix * rework * tweaks * finish * remove overriden available attribute * rework --- homeassistant/components/airvisual/sensor.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 796607f8215..d4b988a0ddc 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -266,9 +266,12 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): self._attr_device_class = device_class self._attr_icon = icon + self._attr_name = ( + f"{coordinator.data['settings']['node_name']} Node/Pro: {name}" + ) + self._attr_unique_id = f"{coordinator.data['serial_number']}_{kind}" self._attr_unit_of_measurement = unit self._kind = kind - self._name = name @property def device_info(self): @@ -284,17 +287,6 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): ), } - @property - def name(self): - """Return the name.""" - node_name = self.coordinator.data["settings"]["node_name"] - return f"{node_name} Node/Pro: {self._name}" - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self.coordinator.data['serial_number']}_{self._kind}" - @callback def update_from_latest_data(self): """Update the entity from the latest data.""" From 668437741a582ce5464c8bd6a9b592dcb2cb6f83 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 08:09:54 -0400 Subject: [PATCH 464/818] Use entity class attributes for Bmw connected drive (#53054) * Use entity class attributes for bmw_connected_driv * forgot the icon --- .../bmw_connected_drive/__init__.py | 26 ++--- .../bmw_connected_drive/binary_sensor.py | 101 ++++++----------- .../bmw_connected_drive/device_tracker.py | 27 +---- .../components/bmw_connected_drive/lock.py | 63 +++-------- .../components/bmw_connected_drive/sensor.py | 103 ++++++------------ 5 files changed, 99 insertions(+), 221 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 599892d6a03..3bd2365f88e 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -317,6 +317,8 @@ class BMWConnectedDriveAccount: class BMWConnectedDriveBaseEntity(Entity): """Common base for BMW entities.""" + _attr_should_poll = False + def __init__(self, account, vehicle): """Initialize sensor.""" self._account = account @@ -326,15 +328,11 @@ class BMWConnectedDriveBaseEntity(Entity): "vin": self._vehicle.vin, ATTR_ATTRIBUTION: ATTRIBUTION, } - - @property - def device_info(self) -> DeviceInfo: - """Return info for device registry.""" - return { - "identifiers": {(DOMAIN, self._vehicle.vin)}, - "name": f'{self._vehicle.attributes.get("brand")} {self._vehicle.name}', - "model": self._vehicle.name, - "manufacturer": self._vehicle.attributes.get("brand"), + self._attr_device_info = { + "identifiers": {(DOMAIN, vehicle.vin)}, + "name": f'{vehicle.attributes.get("brand")} {vehicle.name}', + "model": vehicle.name, + "manufacturer": vehicle.attributes.get("brand"), } @property @@ -342,14 +340,6 @@ class BMWConnectedDriveBaseEntity(Entity): """Return the state attributes of the sensor.""" return self._attrs - @property - def should_poll(self): - """Do not poll this class. - - Updates are triggered from BMWConnectedDriveAccount. - """ - return False - def update_callback(self): """Schedule a state update.""" self.schedule_update_ha_state(True) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index bebb55bbde0..d7f0d150193 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -76,41 +76,45 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): super().__init__(account, vehicle) self._attribute = attribute - self._name = f"{self._vehicle.name} {self._attribute}" - self._unique_id = f"{self._vehicle.vin}-{self._attribute}" + self._attr_name = f"{vehicle.name} {attribute}" + self._attr_unique_id = f"{vehicle.vin}-{attribute}" self._sensor_name = sensor_name - self._device_class = device_class - self._icon = icon - self._state = None + self._attr_device_class = device_class + self._attr_icon = icon - @property - def unique_id(self): - """Return the unique ID of the binary sensor.""" - return self._unique_id + def update(self): + """Read new state data from the library.""" + vehicle_state = self._vehicle.state - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name + # device class opening: On means open, Off means closed + if self._attribute == "lids": + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + self._attr_state = not vehicle_state.all_lids_closed + if self._attribute == "windows": + self._attr_state = not vehicle_state.all_windows_closed + # device class lock: On means unlocked, Off means locked + if self._attribute == "door_lock_state": + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._attr_state = vehicle_state.door_lock_state not in [ + LockState.LOCKED, + LockState.SECURED, + ] + # device class light: On means light detected, Off means no light + if self._attribute == "lights_parking": + self._attr_state = vehicle_state.are_parking_lights_on + # device class problem: On means problem detected, Off means no problem + if self._attribute == "condition_based_services": + self._attr_state = not vehicle_state.are_all_cbs_ok + if self._attribute == "check_control_messages": + self._attr_state = vehicle_state.has_check_control_messages + # device class power: On means power detected, Off means no power + if self._attribute == "charging_status": + self._attr_state = vehicle_state.charging_status in [ChargingState.CHARGING] + # device class plug: On means device is plugged in, + # Off means device is unplugged + if self._attribute == "connection_status": + self._attr_state = vehicle_state.connection_status == "CONNECTED" - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._device_class - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state result = self._attrs.copy() @@ -144,40 +148,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): elif self._attribute == "connection_status": result["connection_status"] = vehicle_state.connection_status - return sorted(result.items()) - - def update(self): - """Read new state data from the library.""" - vehicle_state = self._vehicle.state - - # device class opening: On means open, Off means closed - if self._attribute == "lids": - _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) - self._state = not vehicle_state.all_lids_closed - if self._attribute == "windows": - self._state = not vehicle_state.all_windows_closed - # device class lock: On means unlocked, Off means locked - if self._attribute == "door_lock_state": - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._state = vehicle_state.door_lock_state not in [ - LockState.LOCKED, - LockState.SECURED, - ] - # device class light: On means light detected, Off means no light - if self._attribute == "lights_parking": - self._state = vehicle_state.are_parking_lights_on - # device class problem: On means problem detected, Off means no problem - if self._attribute == "condition_based_services": - self._state = not vehicle_state.are_all_cbs_ok - if self._attribute == "check_control_messages": - self._state = vehicle_state.has_check_control_messages - # device class power: On means power detected, Off means no power - if self._attribute == "charging_status": - self._state = vehicle_state.charging_status in [ChargingState.CHARGING] - # device class plug: On means device is plugged in, - # Off means device is unplugged - if self._attribute == "connection_status": - self._state = vehicle_state.connection_status == "CONNECTED" + self._attr_extra_state_attributes = sorted(result.items()) def _format_cbs_report(self, report): result = {} diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 25adf6cb09f..62b2ed9b9d9 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -29,15 +29,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): """BMW Connected Drive device tracker.""" + _attr_force_update = False + _attr_icon = "mdi:car" + def __init__(self, account, vehicle): """Initialize the Tracker.""" super().__init__(account, vehicle) - self._unique_id = vehicle.vin + self._attr_unique_id = vehicle.vin self._location = ( vehicle.state.gps_position if vehicle.state.gps_position else (None, None) ) - self._name = vehicle.name + self._attr_name = vehicle.name @property def latitude(self): @@ -49,31 +52,11 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): """Return longitude value of the device.""" return self._location[1] if self._location else None - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return the unique ID.""" - return self._unique_id - @property def source_type(self): """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:car" - - @property - def force_update(self): - """All updates do not need to be written to the state machine.""" - return False - def update(self): """Update state of the decvice tracker.""" self._location = ( diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 97c9be7216b..3d27cf833b6 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -4,7 +4,6 @@ import logging from bimmer_connected.state import LockState from homeassistant.components.lock import LockEntity -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity from .const import CONF_ACCOUNT, DATA_ENTRIES @@ -33,50 +32,17 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): super().__init__(account, vehicle) self._attribute = attribute - self._name = f"{self._vehicle.name} {self._attribute}" - self._unique_id = f"{self._vehicle.vin}-{self._attribute}" + self._attr_name = f"{vehicle.name} {attribute}" + self._attr_unique_id = f"{vehicle.vin}-{attribute}" self._sensor_name = sensor_name - self._state = None - self.door_lock_state_available = ( - DOOR_LOCK_STATE in self._vehicle.available_attributes - ) - - @property - def unique_id(self): - """Return the unique ID of the lock.""" - return self._unique_id - - @property - def name(self): - """Return the name of the lock.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the state attributes of the lock.""" - vehicle_state = self._vehicle.state - result = self._attrs.copy() - - if self.door_lock_state_available: - result["door_lock_state"] = vehicle_state.door_lock_state.value - result["last_update_reason"] = vehicle_state.last_update_reason - return result - - @property - def is_locked(self): - """Return true if lock is locked.""" - if self.door_lock_state_available: - result = self._state == STATE_LOCKED - else: - result = None - return result + self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes def lock(self, **kwargs): """Lock the car.""" _LOGGER.debug("%s: locking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the # update callback response - self._state = STATE_LOCKED + self._attr_is_locked = True self.schedule_update_ha_state() self._vehicle.remote_services.trigger_remote_door_lock() @@ -85,18 +51,23 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): _LOGGER.debug("%s: unlocking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the # update callback response - self._state = STATE_UNLOCKED + self._attr_is_locked = False self.schedule_update_ha_state() self._vehicle.remote_services.trigger_remote_door_unlock() def update(self): """Update state of the lock.""" _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) - vehicle_state = self._vehicle.state + if self._vehicle.state.door_lock_state in [LockState.LOCKED, LockState.SECURED]: + self._attr_is_locked = True + else: + self._attr_is_locked = False + if not self.door_lock_state_available: + self._attr_is_locked = None - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._state = ( - STATE_LOCKED - if vehicle_state.door_lock_state in [LockState.LOCKED, LockState.SECURED] - else STATE_UNLOCKED - ) + vehicle_state = self._vehicle.state + result = self._attrs.copy() + if self.door_lock_state_available: + result["door_lock_state"] = vehicle_state.door_lock_state.value + result["last_update_reason"] = vehicle_state.last_update_reason + self._attr_extra_state_attributes = result diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 8d44d1290dc..df899496339 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -503,94 +503,46 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): self._attribute = attribute self._service = service - self._state = None - if self._service: - self._name = ( - f"{self._vehicle.name} {self._service.lower()}_{self._attribute}" - ) - self._unique_id = ( - f"{self._vehicle.vin}-{self._service.lower()}-{self._attribute}" - ) + if service: + self._attr_name = f"{vehicle.name} {service.lower()}_{attribute}" + self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{attribute}" else: - self._name = f"{self._vehicle.name} {self._attribute}" - self._unique_id = f"{self._vehicle.vin}-{self._attribute}" + self._attr_name = f"{vehicle.name} {attribute}" + self._attr_unique_id = f"{vehicle.vin}-{attribute}" self._attribute_info = attribute_info - - @property - def unique_id(self): - """Return the unique ID of the sensor.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - vehicle_state = self._vehicle.state - charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] - - if self._attribute == "charging_level_hv": - return icon_for_battery_level( - battery_level=vehicle_state.charging_level_hv, charging=charging_state - ) - icon = self._attribute_info.get(self._attribute, [None, None, None, None])[0] - return icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - enabled_default = self._attribute_info.get( - self._attribute, [None, None, None, True] + self._attr_entity_registry_enabled_default = attribute_info.get( + attribute, [None, None, None, True] )[3] - return enabled_default - - @property - def state(self): - """Return the state of the sensor. - - The return type of this call depends on the attribute that - is configured. - """ - return self._state - - @property - def device_class(self) -> str: - """Get the device class.""" - clss = self._attribute_info.get(self._attribute, [None, None, None, None])[1] - return clss - - @property - def unit_of_measurement(self) -> str: - """Get the unit of measurement.""" - unit = self._attribute_info.get(self._attribute, [None, None, None, None])[2] - return unit + self._attr_device_class = attribute_info.get( + attribute, [None, None, None, None] + )[1] + self._attr_unit_of_measurement = attribute_info.get( + attribute, [None, None, None, None] + )[2] def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state if self._attribute == "charging_status": - self._state = getattr(vehicle_state, self._attribute).value + self._attr_state = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) - self._state = round(value_converted) + self._attr_state = round(value_converted) elif self.unit_of_measurement == LENGTH_MILES: value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) - self._state = round(value_converted) + self._attr_state = round(value_converted) elif self._service is None: - self._state = getattr(vehicle_state, self._attribute) + self._attr_state = getattr(vehicle_state, self._attribute) elif self._service == SERVICE_LAST_TRIP: vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "date_utc": date_str = getattr(vehicle_last_trip, "date") - self._state = dt_util.parse_datetime(date_str).isoformat() + self._attr_state = dt_util.parse_datetime(date_str).isoformat() else: - self._state = getattr(vehicle_last_trip, self._attribute) + self._attr_state = getattr(vehicle_last_trip, self._attribute) elif self._service == SERVICE_ALL_TRIPS: vehicle_all_trips = self._vehicle.state.all_trips for attribute in ( @@ -603,10 +555,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): if self._attribute.startswith(f"{attribute}_"): attr = getattr(vehicle_all_trips, attribute) sub_attr = self._attribute.replace(f"{attribute}_", "") - self._state = getattr(attr, sub_attr) + self._attr_state = getattr(attr, sub_attr) return if self._attribute == "reset_date_utc": date_str = getattr(vehicle_all_trips, "reset_date") - self._state = dt_util.parse_datetime(date_str).isoformat() + self._attr_state = dt_util.parse_datetime(date_str).isoformat() else: - self._state = getattr(vehicle_all_trips, self._attribute) + self._attr_state = getattr(vehicle_all_trips, self._attribute) + + vehicle_state = self._vehicle.state + charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] + + if self._attribute == "charging_level_hv": + self._attr_icon = icon_for_battery_level( + battery_level=vehicle_state.charging_level_hv, charging=charging_state + ) + self._attr_icon = self._attribute_info.get( + self._attribute, [None, None, None, None] + )[0] From 0803b2aecd537b836338f4ff075afff242deceba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 08:32:42 -0400 Subject: [PATCH 465/818] Use entity class attributes for arest (#52678) --- .../components/arest/binary_sensor.py | 28 +++------- homeassistant/components/arest/sensor.py | 53 ++++++------------- homeassistant/components/arest/switch.py | 44 +++++---------- 3 files changed, 36 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 3cd9038f1a8..d59e6d0cccb 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -73,34 +73,18 @@ class ArestBinarySensor(BinarySensorEntity): def __init__(self, arest, resource, name, device_class, pin): """Initialize the aREST device.""" self.arest = arest - self._resource = resource - self._name = name - self._device_class = device_class - self._pin = pin + self._attr_name = name + self._attr_device_class = device_class - if self._pin is not None: - request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) + if pin is not None: + request = requests.get(f"{resource}/mode/{pin}/i", timeout=10) if request.status_code != HTTP_OK: - _LOGGER.error("Can't set mode of %s", self._resource) - - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return bool(self.arest.data.get("state")) - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class + _LOGGER.error("Can't set mode of %s", resource) def update(self): """Get the latest data from aREST API.""" self.arest.update() + self._attr_is_on = bool(self.arest.data.get("state")) class ArestData: diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 061c15eafb0..7129b989f47 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -139,48 +139,27 @@ class ArestSensor(SensorEntity): ): """Initialize the sensor.""" self.arest = arest - self._resource = resource - self._name = f"{location.title()} {name.title()}" + self._attr_name = f"{location.title()} {name.title()}" self._variable = variable - self._pin = pin - self._state = None - self._unit_of_measurement = unit_of_measurement + self._attr_unit_of_measurement = unit_of_measurement self._renderer = renderer - if self._pin is not None: - request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) + if pin is not None: + request = requests.get(f"{resource}/mode/{pin}/i", timeout=10) if request.status_code != HTTP_OK: - _LOGGER.error("Can't set mode of %s", self._resource) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def state(self): - """Return the state of the sensor.""" - values = self.arest.data - - if "error" in values: - return values["error"] - - value = self._renderer(values.get("value", values.get(self._variable, None))) - return value + _LOGGER.error("Can't set mode of %s", resource) def update(self): """Get the latest data from aREST API.""" self.arest.update() - - @property - def available(self): - """Could the device be accessed during the last update call.""" - return self.arest.available + self._attr_available = self.arest.available + values = self.arest.data + if "error" in values: + self._attr_state = values["error"] + else: + self._attr_state = self._renderer( + values.get("value", values.get(self._variable, None)) + ) class ArestData: @@ -191,7 +170,7 @@ class ArestData: self._resource = resource self._pin = pin self.data = {} - self.available = True + self._attr_available = True @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -212,7 +191,7 @@ class ArestData: f"{self._resource}/digital/{self._pin}", timeout=10 ) self.data = {"value": response.json()["return_value"]} - self.available = True + self._attr_available = True except requests.exceptions.ConnectionError: _LOGGER.error("No route to device %s", self._resource) - self.available = False + self._attr_available = False diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index ddd6b51f76d..d20eb7a5f8d 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -86,24 +86,8 @@ class ArestSwitchBase(SwitchEntity): def __init__(self, resource, location, name): """Initialize the switch.""" self._resource = resource - self._name = f"{location.title()} {name.title()}" - self._state = None - self._available = True - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def available(self): - """Could the device be accessed during the last update call.""" - return self._available + self._attr_name = f"{location.title()} {name.title()}" + self._attr_available = True class ArestSwitchFunction(ArestSwitchBase): @@ -134,7 +118,7 @@ class ArestSwitchFunction(ArestSwitchBase): ) if request.status_code == HTTP_OK: - self._state = True + self._attr_is_on = True else: _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) @@ -145,7 +129,7 @@ class ArestSwitchFunction(ArestSwitchBase): ) if request.status_code == HTTP_OK: - self._state = False + self._attr_is_on = False else: _LOGGER.error( "Can't turn off function %s at %s", self._func, self._resource @@ -155,11 +139,11 @@ class ArestSwitchFunction(ArestSwitchBase): """Get the latest data from aREST API and update the state.""" try: request = requests.get(f"{self._resource}/{self._func}", timeout=10) - self._state = request.json()["return_value"] != 0 - self._available = True + self._attr_is_on = request.json()["return_value"] != 0 + self._attr_available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) - self._available = False + self._attr_available = False class ArestSwitchPin(ArestSwitchBase): @@ -171,10 +155,10 @@ class ArestSwitchPin(ArestSwitchBase): self._pin = pin self.invert = invert - request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10) + request = requests.get(f"{resource}/mode/{pin}/o", timeout=10) if request.status_code != HTTP_OK: _LOGGER.error("Can't set mode") - self._available = False + self._attr_available = False def turn_on(self, **kwargs): """Turn the device on.""" @@ -183,7 +167,7 @@ class ArestSwitchPin(ArestSwitchBase): f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 ) if request.status_code == HTTP_OK: - self._state = True + self._attr_is_on = True else: _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) @@ -194,7 +178,7 @@ class ArestSwitchPin(ArestSwitchBase): f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10 ) if request.status_code == HTTP_OK: - self._state = False + self._attr_is_on = False else: _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) @@ -203,8 +187,8 @@ class ArestSwitchPin(ArestSwitchBase): try: request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) status_value = int(self.invert) - self._state = request.json()["return_value"] != status_value - self._available = True + self._attr_is_on = request.json()["return_value"] != status_value + self._attr_available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) - self._available = False + self._attr_available = False From 800f7fe3a5cd94e405e87e834cf99dcda0fc3948 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 09:27:52 -0400 Subject: [PATCH 466/818] Use entity class attributes for Broadlink (#53058) * Clanup broadlink * rework * tweak * fix using wrong attribute * tweak * revert device info --- homeassistant/components/broadlink/entity.py | 3 +- homeassistant/components/broadlink/remote.py | 4 +- homeassistant/components/broadlink/sensor.py | 12 ++-- homeassistant/components/broadlink/switch.py | 61 +++++++++----------- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index 850611b391f..fc5d22302a6 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -1,11 +1,12 @@ """Broadlink entities.""" from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import Entity from .const import DOMAIN -class BroadlinkEntity: +class BroadlinkEntity(Entity): """Representation of a Broadlink entity.""" _attr_should_poll = False diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index b9dd34d22d8..3bb85ab9d85 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -127,10 +127,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): self._flags = defaultdict(int) self._lock = asyncio.Lock() - self._attr_name = f"{self._device.name} Remote" + self._attr_name = f"{device.name} Remote" self._attr_is_on = True self._attr_supported_features = SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND - self._attr_unique_id = self._device.unique_id + self._attr_unique_id = device.unique_id def _extract_codes(self, commands, device=None): """Extract a list of codes. diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 851668fdeff..044486a4a67 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -77,14 +77,12 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity): self._coordinator = device.update_manager.coordinator self._monitored_condition = monitored_condition - self._attr_device_class = SENSOR_TYPES[self._monitored_condition][2] - self._attr_name = ( - f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}" - ) - self._attr_state_class = SENSOR_TYPES[self._monitored_condition][3] + self._attr_device_class = SENSOR_TYPES[monitored_condition][2] + self._attr_name = f"{device.name} {SENSOR_TYPES[monitored_condition][0]}" + self._attr_state_class = SENSOR_TYPES[monitored_condition][3] self._attr_state = self._coordinator.data[monitored_condition] - self._attr_unique_id = f"{self._device.unique_id}-{self._monitored_condition}" - self._attr_unit_of_measurement = SENSOR_TYPES[self._monitored_condition][1] + self._attr_unique_id = f"{device.unique_id}-{monitored_condition}" + self._attr_unit_of_measurement = SENSOR_TYPES[monitored_condition][1] @callback def update_data(self): diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 1576c8b8418..9f434102b17 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -135,22 +135,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): """Representation of a Broadlink switch.""" + _attr_assumed_state = True + _attr_device_class = DEVICE_CLASS_SWITCH + def __init__(self, device, command_on, command_off): """Initialize the switch.""" super().__init__(device) self._command_on = command_on self._command_off = command_off self._coordinator = device.update_manager.coordinator - self._state = None self._attr_assumed_state = True self._attr_device_class = DEVICE_CLASS_SWITCH - self._attr_name = f"{self._device.name} Switch" - - @property - def is_on(self): - """Return True if the switch is on.""" - return self._state + self._attr_name = f"{device.name} Switch" + self._attr_unique_id = device.unique_id @callback def update_data(self): @@ -159,9 +157,8 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): async def async_added_to_hass(self): """Call when the switch is added to hass.""" - if self._state is None: - state = await self.async_get_last_state() - self._state = state is not None and state.state == STATE_ON + state = await self.async_get_last_state() + self._attr_is_on = state is not None and state.state == STATE_ON self.async_on_remove(self._coordinator.async_add_listener(self.update_data)) async def async_update(self): @@ -171,13 +168,13 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): async def async_turn_on(self, **kwargs): """Turn on the switch.""" if await self._async_send_packet(self._command_on): - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn off the switch.""" if await self._async_send_packet(self._command_off): - self._state = False + self._attr_is_on = False self.async_write_ha_state() @abstractmethod @@ -229,46 +226,41 @@ class BroadlinkSP1Switch(BroadlinkSwitch): class BroadlinkSP2Switch(BroadlinkSP1Switch): """Representation of a Broadlink SP2 switch.""" + _attr_assumed_state = False + def __init__(self, device, *args, **kwargs): """Initialize the switch.""" super().__init__(device, *args, **kwargs) - self._state = self._coordinator.data["pwr"] - self._load_power = self._coordinator.data.get("power") - - self._attr_assumed_state = False - - @property - def current_power_w(self): - """Return the current power usage in Watt.""" - return self._load_power + self._attr_is_on = self._coordinator.data["pwr"] + self._attr_current_power_w = self._coordinator.data.get("power") @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: - self._state = self._coordinator.data["pwr"] - self._load_power = self._coordinator.data.get("power") + self._attr_is_on = self._coordinator.data["pwr"] + self._attr_current_power_w = self._coordinator.data.get("power") self.async_write_ha_state() class BroadlinkMP1Slot(BroadlinkSwitch): """Representation of a Broadlink MP1 slot.""" + _attr_assumed_state = False + def __init__(self, device, slot): """Initialize the switch.""" super().__init__(device, 1, 0) self._slot = slot - self._state = self._coordinator.data[f"s{slot}"] - - self._attr_name = f"{self._device.name} S{self._slot}" - self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}" - self._attr_assumed_state = False + self._attr_is_on = self._coordinator.data[f"s{slot}"] + self._attr_name = f"{device.name} S{slot}" + self._attr_unique_id = f"{device.unique_id}-s{slot}" @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: - self._state = self._coordinator.data[f"s{self._slot}"] + self._attr_is_on = self._coordinator.data[f"s{self._slot}"] self.async_write_ha_state() async def _async_send_packet(self, packet): @@ -286,22 +278,23 @@ class BroadlinkMP1Slot(BroadlinkSwitch): class BroadlinkBG1Slot(BroadlinkSwitch): """Representation of a Broadlink BG1 slot.""" + _attr_assumed_state = False + def __init__(self, device, slot): """Initialize the switch.""" super().__init__(device, 1, 0) self._slot = slot - self._state = self._coordinator.data[f"pwr{slot}"] + self._attr_is_on = self._coordinator.data[f"pwr{slot}"] - self._attr_name = f"{self._device.name} S{self._slot}" + self._attr_name = f"{device.name} S{slot}" self._attr_device_class = DEVICE_CLASS_OUTLET - self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}" - self._attr_assumed_state = False + self._attr_unique_id = f"{device.unique_id}-s{slot}" @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: - self._state = self._coordinator.data[f"pwr{self._slot}"] + self._attr_is_on = self._coordinator.data[f"pwr{self._slot}"] self.async_write_ha_state() async def _async_send_packet(self, packet): From f128bc9ef8cd991a19e5e0f823c7dc6971b27677 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 Jul 2021 18:16:27 +0200 Subject: [PATCH 467/818] Add reauth flow to Synology DSM (#53204) --- .../components/synology_dsm/__init__.py | 35 +++++++++++- .../components/synology_dsm/config_flow.py | 55 +++++++++++++++++-- .../components/synology_dsm/const.py | 2 + .../components/synology_dsm/strings.json | 11 +++- .../synology_dsm/translations/de.json | 11 +++- .../synology_dsm/translations/en.json | 11 +++- .../synology_dsm/test_config_flow.py | 52 +++++++++++++++++- tests/components/synology_dsm/test_init.py | 28 ++++++++++ 8 files changed, 195 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 0bbf5febbc5..9ec56b898ca 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -18,11 +18,15 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, + SynologyDSMLogin2SARequiredException, + SynologyDSMLoginDisabledAccountException, SynologyDSMLoginFailedException, + SynologyDSMLoginInvalidException, + SynologyDSMLoginPermissionDeniedException, SynologyDSMRequestException, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_HOST, @@ -64,6 +68,8 @@ from .const import ( ENTITY_ICON, ENTITY_NAME, ENTITY_UNIT, + EXCEPTION_DETAILS, + EXCEPTION_UNKNOWN, PLATFORMS, SERVICE_REBOOT, SERVICE_SHUTDOWN, @@ -181,6 +187,33 @@ async def async_setup_entry( # noqa: C901 api = SynoApi(hass, entry) try: await api.async_setup() + except ( + SynologyDSMLogin2SARequiredException, + SynologyDSMLoginDisabledAccountException, + SynologyDSMLoginInvalidException, + SynologyDSMLoginPermissionDeniedException, + ) as err: + if err.args[0] and isinstance(err.args[0], dict): + # pylint: disable=no-member + details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) + else: + details = EXCEPTION_UNKNOWN + _LOGGER.debug( + "Reauthentication for DSM '%s' needed - reason: %s", + entry.unique_id, + details, + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "data": {**entry.data}, + EXCEPTION_DETAILS: details, + }, + ) + ) + return False except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: _LOGGER.debug( "Unable to connect to DSM '%s' during setup: %s", entry.unique_id, err diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 5f11f158cec..97f9e4343fa 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -46,6 +46,7 @@ from .const import ( DEFAULT_USE_SSL, DEFAULT_VERIFY_SSL, DOMAIN, + EXCEPTION_DETAILS, ) _LOGGER = logging.getLogger(__name__) @@ -57,6 +58,15 @@ def _discovery_schema_with_defaults(discovery_info: DiscoveryInfoType) -> vol.Sc return vol.Schema(_ordered_shared_schema(discovery_info)) +def _reauth_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")): str, + vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")): str, + } + ) + + def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: user_schema = { vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, @@ -100,6 +110,8 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize the synology_dsm config flow.""" self.saved_user_input: dict[str, Any] = {} self.discovered_conf: dict[str, Any] = {} + self.reauth_conf: dict[str, Any] = {} + self.reauth_reason: str | None = None async def _show_setup_form( self, @@ -110,10 +122,18 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): if not user_input: user_input = {} + description_placeholders = {} + if self.discovered_conf: user_input.update(self.discovered_conf) step_id = "link" data_schema = _discovery_schema_with_defaults(user_input) + description_placeholders = self.discovered_conf + elif self.reauth_conf: + user_input.update(self.reauth_conf) + step_id = "reauth" + data_schema = _reauth_schema_with_defaults(user_input) + description_placeholders = {EXCEPTION_DETAILS: self.reauth_reason} else: step_id = "user" data_schema = _user_schema_with_defaults(user_input) @@ -122,7 +142,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): step_id=step_id, data_schema=data_schema, errors=errors or {}, - description_placeholders=self.discovered_conf or {}, + description_placeholders=description_placeholders, ) async def async_step_user( @@ -137,6 +157,15 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): if self.discovered_conf: user_input.update(self.discovered_conf) + if self.reauth_conf: + self.reauth_conf.update( + { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + ) + user_input.update(self.reauth_conf) + host = user_input[CONF_HOST] port = user_input.get(CONF_PORT) username = user_input[CONF_USERNAME] @@ -181,10 +210,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): return await self._show_setup_form(user_input, errors) # unique_id should be serial for services purpose - await self.async_set_unique_id(serial, raise_on_progress=False) - - # Check if already configured - self._abort_if_unique_id_configured() + existing_entry = await self.async_set_unique_id(serial, raise_on_progress=False) config_data = { CONF_HOST: host, @@ -202,6 +228,15 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): if user_input.get(CONF_VOLUMES): config_data[CONF_VOLUMES] = user_input[CONF_VOLUMES] + if existing_entry and self.reauth_conf: + self.hass.config_entries.async_update_entry( + existing_entry, data=config_data + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + if existing_entry: + return self.async_abort(reason="already_configured") + return self.async_create_entry(title=host, data=config_data) async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: @@ -227,6 +262,16 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_conf = self.context.get("data", {}) + self.reauth_reason = self.context.get(EXCEPTION_DETAILS) + if user_input is None: + return await self.async_step_user() + return await self.async_step_user(user_input) + async def async_step_link(self, user_input: dict[str, Any]) -> FlowResult: """Link a config entry from discovery.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 334832ddf2b..e8b919f09d5 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -37,6 +37,8 @@ COORDINATOR_CAMERAS = "coordinator_cameras" COORDINATOR_CENTRAL = "coordinator_central" COORDINATOR_SWITCHES = "coordinator_switches" SYSTEM_LOADED = "system_loaded" +EXCEPTION_DETAILS = "details" +EXCEPTION_UNKNOWN = "unknown" # Entry keys SYNO_API = "syno_api" diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 1464b8a6a06..6baaaaef9f6 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -29,6 +29,14 @@ "password": "[%key:common::config_flow::data::password%]", "port": "[%key:common::config_flow::data::port%]" } + }, + "reauth": { + "title": "Synology DSM [%key:common::config_flow::title::reauth%]", + "description": "Reason: {details}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -39,7 +47,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 74867aa9044..5a6c52872db 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -29,6 +30,14 @@ "description": "M\u00f6chtest du {name} ({host}) einrichten?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Ursache: {details}", + "title": "Synology DSM erneute Authentifizierung notwendig" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 397bad8b14e..0231f8ddb3c 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -29,6 +30,14 @@ "description": "Do you want to setup {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Reason: {details}", + "title": "Synology DSM Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 0eb9cb66852..cf043c2ce5f 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -23,7 +23,7 @@ from homeassistant.components.synology_dsm.const import ( DEFAULT_VERIFY_SSL, DOMAIN, ) -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -255,6 +255,56 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): assert result["data"].get(CONF_VOLUMES) is None +async def test_reauth(hass: HomeAssistant, service: MagicMock): + """Test reauthentication.""" + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: f"{PASSWORD}_invalid", + }, + unique_id=SERIAL, + ).add_to_hass(hass) + + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=True, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "data": { + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "data": { + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + }, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + async def test_abort_if_already_setup(hass: HomeAssistant, service: MagicMock): """Test we abort if the account is already setup.""" MockConfigEntry( diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 891296d97ea..4d6708a2e79 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -2,7 +2,9 @@ from unittest.mock import patch import pytest +from synology_dsm.exceptions import SynologyDSMLoginInvalidException +from homeassistant import data_entry_flow from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES from homeassistant.const import ( CONF_HOST, @@ -40,3 +42,29 @@ async def test_services_registered(hass: HomeAssistant): assert await hass.config_entries.async_setup(entry.entry_id) for service in SERVICES: assert hass.services.has_service(DOMAIN, service) + + +@pytest.mark.no_bypass_setup +async def test_reauth_triggered(hass: HomeAssistant): + """Test if reauthentication flow is triggered.""" + with patch( + "homeassistant.components.synology_dsm.SynoApi.async_setup", + side_effect=SynologyDSMLoginInvalidException(USERNAME), + ), patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", + return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + ) as mock_async_step_reauth: + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS[0], + }, + ) + entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(entry.entry_id) + mock_async_step_reauth.assert_called_once() From 772cbd59d7b9593d35ed9425fc62b4cc61958884 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 21 Jul 2021 19:11:44 +0200 Subject: [PATCH 468/818] Improve typing in Shelly integration (#52544) --- .strict-typing | 1 + homeassistant/components/shelly/__init__.py | 78 ++++++++---- .../components/shelly/binary_sensor.py | 23 +++- .../components/shelly/config_flow.py | 56 ++++++--- homeassistant/components/shelly/const.py | 80 ++++++------ homeassistant/components/shelly/cover.py | 54 ++++---- .../components/shelly/device_trigger.py | 15 ++- homeassistant/components/shelly/entity.py | 116 +++++++++++------- homeassistant/components/shelly/light.py | 50 ++++---- homeassistant/components/shelly/logbook.py | 16 ++- homeassistant/components/shelly/sensor.py | 44 ++++--- homeassistant/components/shelly/switch.py | 26 ++-- homeassistant/components/shelly/utils.py | 40 +++--- mypy.ini | 11 ++ 14 files changed, 365 insertions(+), 245 deletions(-) diff --git a/.strict-typing b/.strict-typing index 40fb375ca16..735d9ca4a64 100644 --- a/.strict-typing +++ b/.strict-typing @@ -74,6 +74,7 @@ homeassistant.components.remote.* homeassistant.components.scene.* homeassistant.components.select.* homeassistant.components.sensor.* +homeassistant.components.shelly.* homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.ssdp.* diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 425ff11399b..48e27203288 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -1,7 +1,10 @@ """The Shelly integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging +from typing import Any, Final, cast import aioshelly import async_timeout @@ -15,10 +18,11 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, @@ -43,19 +47,19 @@ from .const import ( ) from .utils import get_coap_context, get_device_name, get_device_sleep_period -PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] -SLEEPING_PLATFORMS = ["binary_sensor", "sensor"] -_LOGGER = logging.getLogger(__name__) +PLATFORMS: Final = ["binary_sensor", "cover", "light", "sensor", "switch"] +SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] +_LOGGER: Final = logging.getLogger(__name__) -COAP_SCHEMA = vol.Schema( +COAP_SCHEMA: Final = vol.Schema( { vol.Optional(CONF_COAP_PORT, default=DEFAULT_COAP_PORT): cv.port, } ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Shelly component.""" hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} @@ -113,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sleep_period = entry.data.get("sleep_period") @callback - def _async_device_online(_): + def _async_device_online(_: Any) -> None: _LOGGER.debug("Device %s is online, resuming setup", entry.title) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None @@ -153,7 +157,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_device_setup( hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device -): +) -> None: """Set up a device that is online.""" device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ COAP @@ -174,9 +178,11 @@ async def async_device_setup( class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Wrapper for a Shelly device with Home Assistant specific functions.""" - def __init__(self, hass, entry, device: aioshelly.Device): + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device + ) -> None: """Initialize the Shelly device wrapper.""" - self.device_id = None + self.device_id: str | None = None sleep_period = entry.data["sleep_period"] if sleep_period: @@ -205,7 +211,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) @callback - def _async_device_updates_handler(self): + def _async_device_updates_handler(self) -> None: """Handle device updates.""" if not self.device.initialized: return @@ -258,7 +264,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.name, ) - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch data.""" if self.entry.data.get("sleep_period"): # Sleeping device, no point polling it, just mark it unavailable @@ -267,21 +273,21 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): _LOGGER.debug("Polling Shelly Device - %s", self.name) try: async with async_timeout.timeout(POLLING_TIMEOUT_SEC): - return await self.device.update() + await self.device.update() except OSError as err: raise update_coordinator.UpdateFailed("Error fetching data") from err @property - def model(self): + def model(self) -> str: """Model of the device.""" - return self.entry.data["model"] + return cast(str, self.entry.data["model"]) @property - def mac(self): + def mac(self) -> str: """Mac address of the device.""" - return self.entry.unique_id + return cast(str, self.entry.unique_id) - async def async_setup(self): + async def async_setup(self) -> None: """Set up the wrapper.""" dev_reg = await device_registry.async_get_registry(self.hass) sw_version = self.device.settings["fw"] if self.device.initialized else "" @@ -298,7 +304,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) - def shutdown(self): + def shutdown(self) -> None: """Shutdown the wrapper.""" if self.device: self.device.shutdown() @@ -306,7 +312,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device = None @callback - def _handle_ha_stop(self, _): + def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" _LOGGER.debug("Stopping ShellyDeviceWrapper for %s", self.name) self.shutdown() @@ -315,7 +321,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" - def __init__(self, hass, device: aioshelly.Device): + def __init__(self, hass: HomeAssistant, device: aioshelly.Device) -> None: """Initialize the Shelly device wrapper.""" if ( device.settings["device"]["type"] @@ -335,22 +341,22 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): ) self.device = device - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch data.""" try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): _LOGGER.debug("REST update for %s", self.name) - return await self.device.update_status() + await self.device.update_status() except OSError as err: raise update_coordinator.UpdateFailed("Error fetching data") from err @property - def mac(self): + def mac(self) -> str: """Mac address of the device.""" - return self.device.settings["device"]["mac"] + return cast(str, self.device.settings["device"]["mac"]) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE) if device is not None: @@ -370,3 +376,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) return unload_ok + + +def get_device_wrapper( + hass: HomeAssistant, device_id: str +) -> ShellyDeviceWrapper | None: + """Get a Shelly device wrapper for the given device id.""" + if not hass.data.get(DOMAIN): + return None + + for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]: + wrapper: ShellyDeviceWrapper | None = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry + ].get(COAP) + + if wrapper and wrapper.device_id == device_id: + return wrapper + + return None diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 385b3b30c36..dd1b3a9d66d 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -1,4 +1,8 @@ """Binary sensor for Shelly.""" +from __future__ import annotations + +from typing import Final + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_GAS, @@ -12,6 +16,9 @@ from homeassistant.components.binary_sensor import ( STATE_ON, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ( BlockAttributeDescription, @@ -24,7 +31,7 @@ from .entity import ( ) from .utils import is_momentary_input -SENSORS = { +SENSORS: Final = { ("device", "overtemp"): BlockAttributeDescription( name="Overheating", device_class=DEVICE_CLASS_PROBLEM ), @@ -83,7 +90,7 @@ SENSORS = { ), } -REST_SENSORS = { +REST_SENSORS: Final = { "cloud": RestAttributeDescription( name="Cloud", value=lambda status, _: status["cloud"]["connected"], @@ -103,7 +110,11 @@ REST_SENSORS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up sensors for device.""" if config_entry.data["sleep_period"]: await async_setup_entry_attribute_entities( @@ -130,7 +141,7 @@ class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): """Shelly binary sensor entity.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor state is on.""" return bool(self.attribute_value) @@ -139,7 +150,7 @@ class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity): """Shelly REST binary sensor entity.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if REST sensor state is on.""" return bool(self.attribute_value) @@ -150,7 +161,7 @@ class ShellySleepingBinarySensor( """Represent a shelly sleeping binary sensor.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor state is on.""" if self.block is not None: return bool(self.attribute_value) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 5bf8277066c..c4ddbc0b0aa 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Shelly integration.""" +from __future__ import annotations + import asyncio import logging +from typing import Any, Dict, Final, cast import aiohttp import aioshelly @@ -14,19 +17,23 @@ from homeassistant.const import ( CONF_USERNAME, HTTP_UNAUTHORIZED, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import DiscoveryInfoType from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, DOMAIN from .utils import get_coap_context, get_device_sleep_period -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) +HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) -HTTP_CONNECT_ERRORS = (asyncio.TimeoutError, aiohttp.ClientError) +HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError) -async def validate_input(hass: core.HomeAssistant, host, data): +async def validate_input( + hass: core.HomeAssistant, host: str, data: dict[str, Any] +) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -60,15 +67,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - host = None - info = None - device_info = None + host: str = "" + info: dict[str, Any] = {} + device_info: dict[str, Any] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: - host = user_input[CONF_HOST] + host: str = user_input[CONF_HOST] try: info = await self._async_get_info(host) except HTTP_CONNECT_ERRORS: @@ -106,9 +115,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=HOST_SCHEMA, errors=errors ) - async def async_step_credentials(self, user_input=None): + async def async_step_credentials( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the credentials step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: try: device_info = await validate_input(self.hass, self.host, user_input) @@ -146,7 +157,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="credentials", data_schema=schema, errors=errors ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle zeroconf discovery.""" try: self.info = info = await self._async_get_info(discovery_info["host"]) @@ -173,9 +186,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm_discovery() - async def async_step_confirm_discovery(self, user_input=None): + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle discovery confirm.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: return self.async_create_entry( title=self.device_info["title"] or self.device_info["hostname"], @@ -199,10 +214,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_get_info(self, host): + async def _async_get_info(self, host: str) -> dict[str, Any]: """Get info from shelly device.""" async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await aioshelly.get_info( - aiohttp_client.async_get_clientsession(self.hass), - host, + return cast( + Dict[str, Any], + await aioshelly.get_info( + aiohttp_client.async_get_clientsession(self.hass), + host, + ), ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 119ae478bb7..2c401829c30 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,34 +1,37 @@ """Constants for the Shelly integration.""" +from __future__ import annotations -COAP = "coap" -DATA_CONFIG_ENTRY = "config_entry" -DEVICE = "device" -DOMAIN = "shelly" -REST = "rest" +from typing import Final -CONF_COAP_PORT = "coap_port" -DEFAULT_COAP_PORT = 5683 +COAP: Final = "coap" +DATA_CONFIG_ENTRY: Final = "config_entry" +DEVICE: Final = "device" +DOMAIN: Final = "shelly" +REST: Final = "rest" + +CONF_COAP_PORT: Final = "coap_port" +DEFAULT_COAP_PORT: Final = 5683 # Used in "_async_update_data" as timeout for polling data from devices. -POLLING_TIMEOUT_SEC = 18 +POLLING_TIMEOUT_SEC: Final = 18 # Refresh interval for REST sensors -REST_SENSORS_UPDATE_INTERVAL = 60 +REST_SENSORS_UPDATE_INTERVAL: Final = 60 # Timeout used for aioshelly calls -AIOSHELLY_DEVICE_TIMEOUT_SEC = 10 +AIOSHELLY_DEVICE_TIMEOUT_SEC: Final = 10 # Multiplier used to calculate the "update_interval" for sleeping devices. -SLEEP_PERIOD_MULTIPLIER = 1.2 +SLEEP_PERIOD_MULTIPLIER: Final = 1.2 # Multiplier used to calculate the "update_interval" for non-sleeping devices. -UPDATE_PERIOD_MULTIPLIER = 2.2 +UPDATE_PERIOD_MULTIPLIER: Final = 2.2 # Shelly Air - Maximum work hours before lamp replacement -SHAIR_MAX_WORK_HOURS = 9000 +SHAIR_MAX_WORK_HOURS: Final = 9000 # Map Shelly input events -INPUTS_EVENTS_DICT = { +INPUTS_EVENTS_DICT: Final = { "S": "single", "SS": "double", "SSS": "triple", @@ -38,28 +41,20 @@ INPUTS_EVENTS_DICT = { } # List of battery devices that maintain a permanent WiFi connection -BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] +BATTERY_DEVICES_WITH_PERMANENT_CONNECTION: Final = ["SHMOS-01"] -EVENT_SHELLY_CLICK = "shelly.click" +EVENT_SHELLY_CLICK: Final = "shelly.click" -ATTR_CLICK_TYPE = "click_type" -ATTR_CHANNEL = "channel" -ATTR_DEVICE = "device" -CONF_SUBTYPE = "subtype" +ATTR_CLICK_TYPE: Final = "click_type" +ATTR_CHANNEL: Final = "channel" +ATTR_DEVICE: Final = "device" +CONF_SUBTYPE: Final = "subtype" -BASIC_INPUTS_EVENTS_TYPES = { - "single", - "long", -} +BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"} -SHBTN_INPUTS_EVENTS_TYPES = { - "single", - "double", - "triple", - "long", -} +SHBTN_INPUTS_EVENTS_TYPES: Final = {"single", "double", "triple", "long"} -SUPPORTED_INPUTS_EVENTS_TYPES = SHIX3_1_INPUTS_EVENTS_TYPES = { +SUPPORTED_INPUTS_EVENTS_TYPES: Final = { "single", "double", "triple", @@ -68,23 +63,20 @@ SUPPORTED_INPUTS_EVENTS_TYPES = SHIX3_1_INPUTS_EVENTS_TYPES = { "long_single", } -INPUTS_EVENTS_SUBTYPES = { - "button": 1, - "button1": 1, - "button2": 2, - "button3": 3, -} +SHIX3_1_INPUTS_EVENTS_TYPES = SUPPORTED_INPUTS_EVENTS_TYPES -SHBTN_MODELS = ["SHBTN-1", "SHBTN-2"] +INPUTS_EVENTS_SUBTYPES: Final = {"button": 1, "button1": 1, "button2": 2, "button3": 3} -STANDARD_RGB_EFFECTS = { +SHBTN_MODELS: Final = ["SHBTN-1", "SHBTN-2"] + +STANDARD_RGB_EFFECTS: Final = { 0: "Off", 1: "Meteor Shower", 2: "Gradual Change", 3: "Flash", } -SHBLB_1_RGB_EFFECTS = { +SHBLB_1_RGB_EFFECTS: Final = { 0: "Off", 1: "Meteor Shower", 2: "Gradual Change", @@ -95,8 +87,8 @@ SHBLB_1_RGB_EFFECTS = { } # Kelvin value for colorTemp -KELVIN_MAX_VALUE = 6500 -KELVIN_MIN_VALUE_WHITE = 2700 -KELVIN_MIN_VALUE_COLOR = 3000 +KELVIN_MAX_VALUE: Final = 6500 +KELVIN_MIN_VALUE_WHITE: Final = 2700 +KELVIN_MIN_VALUE_COLOR: Final = 3000 -UPTIME_DEVIATION = 5 +UPTIME_DEVIATION: Final = 5 diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index dc2dba654f3..73b8b1baae3 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -1,4 +1,8 @@ """Cover for Shelly.""" +from __future__ import annotations + +from typing import Any, cast + from aioshelly import Block from homeassistant.components.cover import ( @@ -10,14 +14,20 @@ from homeassistant.components.cover import ( SUPPORT_STOP, CoverEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up cover for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] blocks = [block for block in wrapper.device.blocks if block.type == "roller"] @@ -36,72 +46,72 @@ class ShellyCover(ShellyBlockEntity, CoverEntity): def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize light.""" super().__init__(wrapper, block) - self.control_result = None - self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + self.control_result: dict[str, Any] | None = None + self._supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP if self.wrapper.device.settings["rollers"][0]["positioning"]: self._supported_features |= SUPPORT_SET_POSITION @property - def is_closed(self): + def is_closed(self) -> bool: """If cover is closed.""" if self.control_result: - return self.control_result["current_pos"] == 0 + return cast(bool, self.control_result["current_pos"] == 0) - return self.block.rollerPos == 0 + return cast(bool, self.block.rollerPos == 0) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Position of the cover.""" if self.control_result: - return self.control_result["current_pos"] + return cast(int, self.control_result["current_pos"]) - return self.block.rollerPos + return cast(int, self.block.rollerPos) @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing.""" if self.control_result: - return self.control_result["state"] == "close" + return cast(bool, self.control_result["state"] == "close") - return self.block.roller == "close" + return cast(bool, self.block.roller == "close") @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening.""" if self.control_result: - return self.control_result["state"] == "open" + return cast(bool, self.control_result["state"] == "open") - return self.block.roller == "open" + return cast(bool, self.block.roller == "open") @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" self.control_result = await self.set_state(go="close") self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" self.control_result = await self.set_state(go="open") self.async_write_ha_state() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" self.control_result = await self.set_state( go="to_pos", roller_pos=kwargs[ATTR_POSITION] ) self.async_write_ha_state() - async def async_stop_cover(self, **_kwargs): + async def async_stop_cover(self, **_kwargs: Any) -> None: """Stop the cover.""" self.control_result = await self.set_state(go="stop") self.async_write_ha_state() @callback - def _update_callback(self): + def _update_callback(self) -> None: """When device updates, clear control result that overrides state.""" self.control_result = None super()._update_callback() diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index e767f49bcbb..bcb909555a9 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -1,6 +1,8 @@ """Provides device triggers for Shelly.""" from __future__ import annotations +from typing import Any, Final + import voluptuous as vol from homeassistant.components.automation import AutomationActionType @@ -20,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_device_wrapper from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -31,9 +34,9 @@ from .const import ( SHBTN_MODELS, SUPPORTED_INPUTS_EVENTS_TYPES, ) -from .utils import get_device_wrapper, get_input_triggers +from .utils import get_input_triggers -TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES), @@ -41,7 +44,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, config: dict[str, Any] +) -> dict[str, Any]: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -62,7 +67,9 @@ async def async_validate_trigger_config(hass, config): ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device triggers for Shelly devices.""" triggers = [] diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 744272ccf91..743dd07414e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -4,31 +4,39 @@ from __future__ import annotations import asyncio from dataclasses import dataclass import logging -from typing import Any, Callable +from typing import Any, Callable, Final, cast import aioshelly import async_timeout from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( device_registry, entity, entity_registry, update_coordinator, ) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import StateType from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, COAP, DATA_CONFIG_ENTRY, DOMAIN, REST from .utils import async_remove_shelly_entity, get_entity_name -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) async def async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, sensors, sensor_class -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + sensors: dict[tuple[str, str], BlockAttributeDescription], + sensor_class: Callable, +) -> None: """Set up entities for attributes.""" wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id @@ -45,8 +53,12 @@ async def async_setup_entry_attribute_entities( async def async_setup_block_attribute_entities( - hass, async_add_entities, wrapper, sensors, sensor_class -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + wrapper: ShellyDeviceWrapper, + sensors: dict[tuple[str, str], BlockAttributeDescription], + sensor_class: Callable, +) -> None: """Set up entities for block attributes.""" blocks = [] @@ -82,8 +94,13 @@ async def async_setup_block_attribute_entities( async def async_restore_block_attribute_entities( - hass, config_entry, async_add_entities, wrapper, sensors, sensor_class -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + wrapper: ShellyDeviceWrapper, + sensors: dict[tuple[str, str], BlockAttributeDescription], + sensor_class: Callable, +) -> None: """Restore block attributes entities.""" entities = [] @@ -117,8 +134,12 @@ async def async_restore_block_attribute_entities( async def async_setup_entry_rest( - hass, config_entry, async_add_entities, sensors, sensor_class -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + sensors: dict[str, RestAttributeDescription], + sensor_class: Callable, +) -> None: """Set up entities for REST sensors.""" wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id @@ -177,53 +198,53 @@ class RestAttributeDescription: class ShellyBlockEntity(entity.Entity): """Helper class to represent a block.""" - def __init__(self, wrapper: ShellyDeviceWrapper, block): + def __init__(self, wrapper: ShellyDeviceWrapper, block: aioshelly.Block) -> None: """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block - self._name: str | None = get_entity_name(wrapper.device, block) + self._name = get_entity_name(wrapper.device, block) @property - def name(self): + def name(self) -> str: """Name of entity.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """If device should be polled.""" return False @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info.""" return { "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} } @property - def available(self): + def available(self) -> bool: """Available.""" return self.wrapper.last_update_success @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID of entity.""" return f"{self.wrapper.mac}-{self.block.description}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) - async def async_update(self): + async def async_update(self) -> None: """Update entity with latest info.""" await self.wrapper.async_request_refresh() @callback - def _update_callback(self): + def _update_callback(self) -> None: """Handle device update.""" self.async_write_ha_state() - async def set_state(self, **kwargs): + async def set_state(self, **kwargs: Any) -> Any: """Set block state (HTTP request).""" _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: @@ -261,16 +282,16 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): unit = unit(block.info(attribute)) self._unit: None | str | Callable[[dict], str] = unit - self._unique_id: None | str = f"{super().unique_id}-{self.attribute}" + self._unique_id: str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID of entity.""" return self._unique_id @property - def name(self): + def name(self) -> str: """Name of sensor.""" return self._name @@ -280,27 +301,27 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.description.default_enabled @property - def attribute_value(self): + def attribute_value(self) -> StateType: """Value of sensor.""" value = getattr(self.block, self.attribute) if value is None: return None - return self.description.value(value) + return cast(StateType, self.description.value(value)) @property - def device_class(self): + def device_class(self) -> str | None: """Device class of sensor.""" return self.description.device_class @property - def icon(self): + def icon(self) -> str | None: """Icon of sensor.""" return self.description.icon @property - def available(self): + def available(self) -> bool: """Available.""" available = super().available @@ -310,7 +331,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.description.available(self.block) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.description.extra_state_attributes is None: return None @@ -336,12 +357,12 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): self._last_value = None @property - def name(self): + def name(self) -> str: """Name of sensor.""" return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info.""" return { "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} @@ -353,35 +374,36 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return self.description.default_enabled @property - def available(self): + def available(self) -> bool: """Available.""" return self.wrapper.last_update_success @property - def attribute_value(self): + def attribute_value(self) -> StateType: """Value of sensor.""" - self._last_value = self.description.value( - self.wrapper.device.status, self._last_value - ) + if callable(self.description.value): + self._last_value = self.description.value( + self.wrapper.device.status, self._last_value + ) return self._last_value @property - def device_class(self): + def device_class(self) -> str | None: """Device class of sensor.""" return self.description.device_class @property - def icon(self): + def icon(self) -> str | None: """Icon of sensor.""" return self.description.icon @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID of entity.""" return f"{self.wrapper.mac}-{self.attribute}" @property - def extra_state_attributes(self) -> dict | None: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.description.extra_state_attributes is None: return None @@ -400,11 +422,11 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti attribute: str, description: BlockAttributeDescription, entry: entity_registry.RegistryEntry | None = None, - sensors: set | None = None, + sensors: dict[tuple[str, str], BlockAttributeDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" self.sensors = sensors - self.last_state = None + self.last_state: StateType = None self.wrapper = wrapper self.attribute = attribute self.block = block @@ -421,9 +443,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti ) elif entry is not None: self._unique_id = entry.unique_id - self._name = entry.original_name + self._name = cast(str, entry.original_name) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() @@ -434,7 +456,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self.description.state_class = last_state.attributes.get(ATTR_STATE_CLASS) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Handle device update.""" if ( self.block is not None diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 8314650d548..047a105a30f 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import Any +from typing import Any, Final, cast from aioshelly import Block import async_timeout @@ -23,7 +23,9 @@ from homeassistant.components.light import ( LightEntity, brightness_supported, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, @@ -44,10 +46,14 @@ from .const import ( from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up lights for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] @@ -78,12 +84,12 @@ class ShellyLight(ShellyBlockEntity, LightEntity): def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize light.""" super().__init__(wrapper, block) - self.control_result = None - self.mode_result = None - self._supported_color_modes = set() - self._supported_features = 0 - self._min_kelvin = KELVIN_MIN_VALUE_WHITE - self._max_kelvin = KELVIN_MAX_VALUE + self.control_result: dict[str, Any] | None = None + self.mode_result: dict[str, Any] | None = None + self._supported_color_modes: set[str] = set() + self._supported_features: int = 0 + self._min_kelvin: int = KELVIN_MIN_VALUE_WHITE + self._max_kelvin: int = KELVIN_MAX_VALUE if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._min_kelvin = KELVIN_MIN_VALUE_COLOR @@ -113,18 +119,18 @@ class ShellyLight(ShellyBlockEntity, LightEntity): def is_on(self) -> bool: """If light is on.""" if self.control_result: - return self.control_result["ison"] + return cast(bool, self.control_result["ison"]) - return self.block.output + return bool(self.block.output) @property - def mode(self) -> str | None: + def mode(self) -> str: """Return the color mode of the light.""" if self.mode_result: - return self.mode_result["mode"] + return cast(str, self.mode_result["mode"]) if hasattr(self.block, "mode"): - return self.block.mode + return cast(str, self.block.mode) if ( hasattr(self.block, "red") @@ -136,7 +142,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return "white" @property - def brightness(self) -> int | None: + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" if self.mode == "color": if self.control_result: @@ -152,7 +158,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return round(255 * brightness_pct / 100) @property - def color_mode(self) -> str | None: + def color_mode(self) -> str: """Return the color mode of the light.""" if self.mode == "color": if hasattr(self.block, "white"): @@ -191,7 +197,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return (*self.rgb_color, white) @property - def color_temp(self) -> int | None: + def color_temp(self) -> int: """Return the CT color value in mireds.""" if self.control_result: color_temp = self.control_result["temp"] @@ -244,7 +250,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return STANDARD_RGB_EFFECTS[effect_index] - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if self.block.type == "relay": self.control_result = await self.set_state(turn="on") @@ -304,12 +310,12 @@ class ShellyLight(ShellyBlockEntity, LightEntity): self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" self.control_result = await self.set_state(turn="off") self.async_write_ha_state() - async def set_light_mode(self, set_mode): + async def set_light_mode(self, set_mode: str | None) -> bool: """Change device mode color/white if mode has changed.""" if set_mode is None or self.mode == set_mode: return True @@ -331,7 +337,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return True @callback - def _update_callback(self): + def _update_callback(self) -> None: """When device updates, clear control & mode result that overrides state.""" self.control_result = None self.mode_result = None diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index 78a5c279a93..5b0ada6f166 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -1,8 +1,13 @@ """Describe Shelly logbook events.""" +from __future__ import annotations + +from typing import Callable from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import EventType +from . import get_device_wrapper from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -10,15 +15,18 @@ from .const import ( DOMAIN, EVENT_SHELLY_CLICK, ) -from .utils import get_device_name, get_device_wrapper +from .utils import get_device_name @callback -def async_describe_events(hass, async_describe_event): +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[EventType], dict]], None], +) -> None: """Describe logbook events.""" @callback - def async_describe_shelly_click_event(event): + def async_describe_shelly_click_event(event: EventType) -> dict[str, str]: """Describe shelly.click logbook event.""" wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID]) if wrapper: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 8a435c3e50f..96ff6e55f8d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,6 +1,11 @@ """Sensor for Shelly.""" +from __future__ import annotations + +from typing import Final, cast + from homeassistant.components import sensor from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEGREE, @@ -12,6 +17,9 @@ from homeassistant.const import ( POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import SHAIR_MAX_WORK_HOURS from .entity import ( @@ -25,7 +33,7 @@ from .entity import ( ) from .utils import get_device_uptime, temperature_unit -SENSORS = { +SENSORS: Final = { ("device", "battery"): BlockAttributeDescription( name="Battery", unit=PERCENTAGE, @@ -153,7 +161,7 @@ SENSORS = { value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, state_class=sensor.STATE_CLASS_MEASUREMENT, - available=lambda block: block.extTemp != 999, + available=lambda block: cast(bool, block.extTemp != 999), ), ("sensor", "humidity"): BlockAttributeDescription( name="Humidity", @@ -161,7 +169,7 @@ SENSORS = { value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_HUMIDITY, state_class=sensor.STATE_CLASS_MEASUREMENT, - available=lambda block: block.extTemp != 999, + available=lambda block: cast(bool, block.extTemp != 999), ), ("sensor", "luminosity"): BlockAttributeDescription( name="Luminosity", @@ -199,7 +207,7 @@ SENSORS = { ), } -REST_SENSORS = { +REST_SENSORS: Final = { "rssi": RestAttributeDescription( name="RSSI", unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -217,7 +225,11 @@ REST_SENSORS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up sensors for device.""" if config_entry.data["sleep_period"]: await async_setup_entry_attribute_entities( @@ -236,36 +248,36 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" @property - def state(self): + def state(self) -> StateType: """Return value of sensor.""" return self.attribute_value @property - def state_class(self): + def state_class(self) -> str | None: """State class of sensor.""" return self.description.state_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" - return self._unit + return cast(str, self._unit) class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): """Represent a shelly REST sensor.""" @property - def state(self): + def state(self) -> StateType: """Return value of sensor.""" return self.attribute_value @property - def state_class(self): + def state_class(self) -> str | None: """State class of sensor.""" return self.description.state_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return self.description.unit @@ -274,7 +286,7 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Represent a shelly sleeping sensor.""" @property - def state(self): + def state(self) -> StateType: """Return value of sensor.""" if self.block is not None: return self.attribute_value @@ -282,11 +294,11 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.last_state @property - def state_class(self): + def state_class(self) -> str | None: """State class of sensor.""" return self.description.state_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" - return self._unit + return cast(str, self._unit) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 6f3dd0b0136..3e35ba878e4 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,8 +1,14 @@ """Switch for Shelly.""" +from __future__ import annotations + +from typing import Any, cast + from aioshelly import Block from homeassistant.components.switch import SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN @@ -10,7 +16,11 @@ from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up switches for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] @@ -50,28 +60,28 @@ class RelaySwitch(ShellyBlockEntity, SwitchEntity): def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize relay switch.""" super().__init__(wrapper, block) - self.control_result = None + self.control_result: dict[str, Any] | None = None @property def is_on(self) -> bool: """If switch is on.""" if self.control_result: - return self.control_result["ison"] + return cast(bool, self.control_result["ison"]) - return self.block.output + return bool(self.block.output) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on relay.""" self.control_result = await self.set_state(turn="on") self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off relay.""" self.control_result = await self.set_state(turn="off") self.async_write_ha_state() @callback - def _update_callback(self): + def _update_callback(self) -> None: """When device updates, clear control result that overrides state.""" self.control_result = None super()._update_callback() diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 37b34dfe9e8..d8ce5ae9e45 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -3,19 +3,19 @@ from __future__ import annotations from datetime import datetime, timedelta import logging +from typing import Any, Final, cast import aioshelly from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import singleton +from homeassistant.helpers.typing import EventType from homeassistant.util.dt import utcnow from .const import ( BASIC_INPUTS_EVENTS_TYPES, - COAP, CONF_COAP_PORT, - DATA_CONFIG_ENTRY, DEFAULT_COAP_PORT, DOMAIN, SHBTN_INPUTS_EVENTS_TYPES, @@ -24,10 +24,12 @@ from .const import ( UPTIME_DEVIATION, ) -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -async def async_remove_shelly_entity(hass, domain, unique_id): +async def async_remove_shelly_entity( + hass: HomeAssistant, domain: str, unique_id: str +) -> None: """Remove a Shelly entity.""" entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) @@ -36,7 +38,7 @@ async def async_remove_shelly_entity(hass, domain, unique_id): entity_reg.async_remove(entity_id) -def temperature_unit(block_info: dict) -> str: +def temperature_unit(block_info: dict[str, Any]) -> str: """Detect temperature unit.""" if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F": return TEMP_FAHRENHEIT @@ -45,7 +47,7 @@ def temperature_unit(block_info: dict) -> str: def get_device_name(device: aioshelly.Device) -> str: """Naming for device.""" - return device.settings["name"] or device.settings["device"]["hostname"] + return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) -> int: @@ -96,7 +98,7 @@ def get_device_channel_name( ): return entity_name - channel_name = None + channel_name: str | None = None mode = block.type + "s" if mode in device.settings: channel_name = device.settings[mode][int(block.channel)].get("name") @@ -112,7 +114,7 @@ def get_device_channel_name( return f"{entity_name} channel {chr(int(block.channel)+base)}" -def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: +def is_momentary_input(settings: dict[str, Any], block: aioshelly.Block) -> bool: """Return true if input button settings is set to a momentary type.""" # Shelly Button type is fixed to momentary and no btn_type if settings["device"]["type"] in SHBTN_MODELS: @@ -134,7 +136,7 @@ def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: return button_type in ["momentary", "momentary_on_release"] -def get_device_uptime(status: dict, last_uptime: str) -> str: +def get_device_uptime(status: dict[str, Any], last_uptime: str) -> str: """Return device uptime string, tolerate up to 5 seconds deviation.""" delta_uptime = utcnow() - timedelta(seconds=status["uptime"]) @@ -178,22 +180,8 @@ def get_input_triggers( return triggers -def get_device_wrapper(hass: HomeAssistant, device_id: str): - """Get a Shelly device wrapper for the given device id.""" - if not hass.data.get(DOMAIN): - return None - - for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]: - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(COAP) - - if wrapper and wrapper.device_id == device_id: - return wrapper - - return None - - @singleton.singleton("shelly_coap") -async def get_coap_context(hass): +async def get_coap_context(hass: HomeAssistant) -> aioshelly.COAP: """Get CoAP context to be used in all Shelly devices.""" context = aioshelly.COAP() if DOMAIN in hass.data: @@ -204,7 +192,7 @@ async def get_coap_context(hass): await context.initialize(port) @callback - def shutdown_listener(ev): + def shutdown_listener(ev: EventType) -> None: context.close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) @@ -212,7 +200,7 @@ async def get_coap_context(hass): return context -def get_device_sleep_period(settings: dict) -> int: +def get_device_sleep_period(settings: dict[str, Any]) -> int: """Return the device sleep period in seconds or 0 for non sleeping devices.""" sleep_period = 0 diff --git a/mypy.ini b/mypy.ini index cf85d2e60a9..22cd7f4a1e0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -825,6 +825,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.shelly.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.slack.*] check_untyped_defs = true disallow_incomplete_defs = true From 2d48d273a79b0c321e6aeaf1f915942d70758f47 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 21 Jul 2021 19:12:32 +0200 Subject: [PATCH 469/818] Fix incorrect unit (#53274) --- homeassistant/components/fritz/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 031f7bc555c..215848251e9 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntit from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_GIGABYTES, + DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, ) @@ -113,14 +114,14 @@ SENSOR_DATA = { state_provider=_retrieve_kb_s_received_state, ), "max_kb_s_sent": SensorData( - name="Max kB/s sent", - unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + name="Max kbit/s sent", + unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_max_kb_s_sent_state, ), "max_kb_s_received": SensorData( - name="Max kB/s received", - unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + name="Max kbit/s received", + unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", state_provider=_retrieve_max_kb_s_received_state, ), From db1a8e9336ca0faf31b7654ba15be449ab28df21 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 21 Jul 2021 19:31:51 +0200 Subject: [PATCH 470/818] Fix similar network names for Fritz (#53278) --- homeassistant/components/fritz/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 2969095d34d..b1ec63e0ce9 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -13,6 +13,7 @@ from fritzconnection.core.exceptions import ( FritzSecurityError, FritzServiceError, ) +import slugify as unicode_slug import xmltodict from homeassistant.components.switch import SwitchEntity @@ -247,7 +248,7 @@ def wifi_entities_list( ) if network_info: ssid = network_info["NewSSID"] - if ssid in networks.values(): + if unicode_slug.slugify(ssid, lowercase=False) in networks.values(): networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}' else: networks[i] = ssid From a1df3519db58d055d3ae0ac03ac90e346495bc48 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 13:37:12 -0400 Subject: [PATCH 471/818] Use entity class attributes for Bsblan (#53165) --- .coveragerc | 2 - homeassistant/components/bsblan/climate.py | 117 +++++---------------- 2 files changed, 29 insertions(+), 90 deletions(-) diff --git a/.coveragerc b/.coveragerc index 83212125cb7..44bb49e5f57 100644 --- a/.coveragerc +++ b/.coveragerc @@ -132,9 +132,7 @@ omit = homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/cover.py - homeassistant/components/bsblan/__init__.py homeassistant/components/bsblan/climate.py - homeassistant/components/bsblan/const.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 3aa3679c6c9..160c4f9d9b3 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -27,7 +27,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -88,6 +87,10 @@ async def async_setup_entry( class BSBLanClimate(ClimateEntity): """Defines a BSBLan climate device.""" + _attr_supported_features = SUPPORT_FLAGS + _attr_hvac_modes = HVAC_MODES + _attr_preset_modes = PRESET_MODES + def __init__( self, entry_id: str, @@ -95,89 +98,33 @@ class BSBLanClimate(ClimateEntity): info: Info, ) -> None: """Initialize BSBLan climate device.""" - self._current_temperature: float | None = None - self._available = True - self._hvac_mode: str | None = None - self._target_temperature: float | None = None - self._temperature_unit = None - self._preset_mode: str | None = None + self._attr_available = True self._store_hvac_mode = None - self._info: Info = info self.bsblan = bsblan - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._info.device_identification - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return self._info.device_identification - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement which this thermostat uses.""" - if self._temperature_unit == "°C": - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_FLAGS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def hvac_mode(self): - """Return the current operation mode.""" - return self._hvac_mode - - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return HVAC_MODES - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def preset_modes(self): - """List of available preset modes.""" - return PRESET_MODES - - @property - def preset_mode(self): - """Return the preset_mode.""" - return self._preset_mode + self._attr_name = self._attr_unique_id = info.device_identification + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, info.device_identification)}, + ATTR_NAME: "BSBLan Device", + ATTR_MANUFACTURER: "BSBLan", + ATTR_MODEL: info.controller_variant, + } async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" _LOGGER.debug("Setting preset mode to: %s", preset_mode) if preset_mode == PRESET_NONE: # restore previous hvac mode - self._hvac_mode = self._store_hvac_mode + self._attr_hvac_mode = self._store_hvac_mode else: # Store hvac mode. - self._store_hvac_mode = self._hvac_mode + self._store_hvac_mode = self._attr_hvac_mode await self.async_set_data(preset_mode=preset_mode) async def async_set_hvac_mode(self, hvac_mode): """Set HVAC mode.""" _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) # preset should be none when hvac mode is set - self._preset_mode = PRESET_NONE + self._attr_preset_mode = PRESET_NONE await self.async_set_data(hvac_mode=hvac_mode) async def async_set_temperature(self, **kwargs): @@ -204,39 +151,33 @@ class BSBLanClimate(ClimateEntity): await self.bsblan.thermostat(**data) except BSBLanError: _LOGGER.error("An error occurred while updating the BSBLan device") - self._available = False + self._attr_available = False async def async_update(self) -> None: """Update BSBlan entity.""" try: state: State = await self.bsblan.state() except BSBLanError: - if self._available: + if self.available: _LOGGER.error("An error occurred while updating the BSBLan device") - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True - self._current_temperature = float(state.current_temperature.value) - self._target_temperature = float(state.target_temperature.value) + self._attr_current_temperature = float(state.current_temperature.value) + self._attr_target_temperature = float(state.target_temperature.value) # check if preset is active else get hvac mode _LOGGER.debug("state hvac/preset mode: %s", state.hvac_mode.value) if state.hvac_mode.value == "2": - self._preset_mode = PRESET_ECO + self._attr_preset_mode = PRESET_ECO else: - self._hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value] - self._preset_mode = PRESET_NONE + self._attr_hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value] + self._attr_preset_mode = PRESET_NONE - self._temperature_unit = state.current_temperature.unit - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this BSBLan device.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)}, - ATTR_NAME: "BSBLan Device", - ATTR_MANUFACTURER: "BSBLan", - ATTR_MODEL: self._info.controller_variant, - } + self._attr_temperature_unit = ( + TEMP_CELSIUS + if state.current_temperature.unit == "°C" + else TEMP_FAHRENHEIT + ) From aed7cb91205d69e213220ba27aed20314657cea8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 21 Jul 2021 19:42:30 +0200 Subject: [PATCH 472/818] Convert skybell to use NamedTuple (#53269) * Convert to NamedTuple. * Second version. * Use names instead of index. * Review comments. * Add meta variable. * Review comment. * Review comments. --- .../components/skybell/binary_sensor.py | 39 ++++++++++++------- homeassistant/components/skybell/switch.py | 30 ++++++++------ 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 7e075fba38a..c6e8200c812 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -1,5 +1,8 @@ """Binary sensor support for the Skybell HD Doorbell.""" +from __future__ import annotations + from datetime import timedelta +from typing import NamedTuple import voluptuous as vol @@ -16,10 +19,26 @@ from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice SCAN_INTERVAL = timedelta(seconds=10) -# Sensor types: Name, device_class, event + +class SkybellBinarySensorMetadata(NamedTuple): + """Metadata for an individual Skybell binary_sensor.""" + + name: str + device_class: str + event: str + + SENSOR_TYPES = { - "button": ["Button", DEVICE_CLASS_OCCUPANCY, "device:sensor:button"], - "motion": ["Motion", DEVICE_CLASS_MOTION, "device:sensor:motion"], + "button": SkybellBinarySensorMetadata( + "Button", + device_class=DEVICE_CLASS_OCCUPANCY, + event="device:sensor:button", + ), + "motion": SkybellBinarySensorMetadata( + "Motion", + device_class=DEVICE_CLASS_MOTION, + event="device:sensor:motion", + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -53,18 +72,12 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): """Initialize a binary sensor for a Skybell device.""" super().__init__(device) self._sensor_type = sensor_type - self._name = "{} {}".format( - self._device.name, SENSOR_TYPES[self._sensor_type][0] - ) - self._device_class = SENSOR_TYPES[self._sensor_type][1] + self._metadata = SENSOR_TYPES[self._sensor_type] + self._attr_name = f"{self._device.name} {self._metadata.name}" + self._device_class = self._metadata.device_class self._event = {} self._state = None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def is_on(self): """Return True if the binary sensor is on.""" @@ -88,7 +101,7 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): """Get the latest data and updates the state.""" super().update() - event = self._device.latest(SENSOR_TYPES[self._sensor_type][2]) + event = self._device.latest(self._metadata.event) self._state = bool(event and event.get("id") != self._event.get("id")) diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index 1ad13af9249..5f9706de4d1 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -1,4 +1,8 @@ """Switch support for the Skybell HD Doorbell.""" +from __future__ import annotations + +from typing import NamedTuple + import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -7,10 +11,20 @@ import homeassistant.helpers.config_validation as cv from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice -# Switch types: Name + +class SkybellSwitchMetadata(NamedTuple): + """Metadata for an individual Skybell switch.""" + + name: str + + SWITCH_TYPES = { - "do_not_disturb": ["Do Not Disturb"], - "motion_sensor": ["Motion Sensor"], + "do_not_disturb": SkybellSwitchMetadata( + "Do Not Disturb", + ), + "motion_sensor": SkybellSwitchMetadata( + "Motion Sensor", + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -44,14 +58,8 @@ class SkybellSwitch(SkybellDevice, SwitchEntity): """Initialize a light for a Skybell device.""" super().__init__(device) self._switch_type = switch_type - self._name = "{} {}".format( - self._device.name, SWITCH_TYPES[self._switch_type][0] - ) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name + metadata = SWITCH_TYPES[self._switch_type] + self._attr_name = f"{self._device.name} {metadata.name}" def turn_on(self, **kwargs): """Turn on the switch.""" From 217c625c9be7a1ed02e1aece39b293f7bac0b5c5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 21 Jul 2021 19:43:33 +0200 Subject: [PATCH 473/818] Convert ebox to use NamedTuple (#53272) * Convert to use NamedTuple. * Convert to NamedTuple. * Use _attr variables. * Review comments. --- homeassistant/components/ebox/sensor.py | 115 ++++++++++++++++-------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 72d169f389e..66a5beda3d2 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -3,8 +3,11 @@ Support for EBox. Get data from 'My Usage Page' page: https://client.ebox.ca/myusage """ +from __future__ import annotations + from datetime import timedelta import logging +from typing import NamedTuple from pyebox import EboxClient from pyebox.client import PyEboxError @@ -34,24 +37,81 @@ REQUESTS_TIMEOUT = 15 SCAN_INTERVAL = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + +class EboxSensorMetadata(NamedTuple): + """Metadata for an individual ebox sensor.""" + + name: str + unit_of_measurement: str + icon: str + + SENSOR_TYPES = { - "usage": ["Usage", PERCENTAGE, "mdi:percent"], - "balance": ["Balance", PRICE, "mdi:cash-usd"], - "limit": ["Data limit", DATA_GIGABITS, "mdi:download"], - "days_left": ["Days left", TIME_DAYS, "mdi:calendar-today"], - "before_offpeak_download": [ + "usage": EboxSensorMetadata( + "Usage", + unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + ), + "balance": EboxSensorMetadata( + "Balance", + unit_of_measurement=PRICE, + icon="mdi:cash-usd", + ), + "limit": EboxSensorMetadata( + "Data limit", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), + "days_left": EboxSensorMetadata( + "Days left", + unit_of_measurement=TIME_DAYS, + icon="mdi:calendar-today", + ), + "before_offpeak_download": EboxSensorMetadata( "Download before offpeak", - DATA_GIGABITS, - "mdi:download", - ], - "before_offpeak_upload": ["Upload before offpeak", DATA_GIGABITS, "mdi:upload"], - "before_offpeak_total": ["Total before offpeak", DATA_GIGABITS, "mdi:download"], - "offpeak_download": ["Offpeak download", DATA_GIGABITS, "mdi:download"], - "offpeak_upload": ["Offpeak Upload", DATA_GIGABITS, "mdi:upload"], - "offpeak_total": ["Offpeak Total", DATA_GIGABITS, "mdi:download"], - "download": ["Download", DATA_GIGABITS, "mdi:download"], - "upload": ["Upload", DATA_GIGABITS, "mdi:upload"], - "total": ["Total", DATA_GIGABITS, "mdi:download"], + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), + "before_offpeak_upload": EboxSensorMetadata( + "Upload before offpeak", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:upload", + ), + "before_offpeak_total": EboxSensorMetadata( + "Total before offpeak", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), + "offpeak_download": EboxSensorMetadata( + "Offpeak download", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), + "offpeak_upload": EboxSensorMetadata( + "Offpeak Upload", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:upload", + ), + "offpeak_total": EboxSensorMetadata( + "Offpeak Total", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), + "download": EboxSensorMetadata( + "Download", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), + "upload": EboxSensorMetadata( + "Upload", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:upload", + ), + "total": EboxSensorMetadata( + "Total", + unit_of_measurement=DATA_GIGABITS, + icon="mdi:download", + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -94,34 +154,19 @@ class EBoxSensor(SensorEntity): def __init__(self, ebox_data, sensor_type, name): """Initialize the sensor.""" - self.client_name = name self.type = sensor_type - self._name = SENSOR_TYPES[sensor_type][0] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._icon = SENSOR_TYPES[sensor_type][2] + metadata = SENSOR_TYPES[sensor_type] + self._attr_name = f"{name} {metadata.name}" + self._attr_unit_of_measurement = metadata.unit_of_measurement + self._attr_icon = metadata.icon self.ebox_data = ebox_data self._state = None - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - @property def state(self): """Return the state of the sensor.""" return self._state - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - async def async_update(self): """Get the latest data from EBox and update the state.""" await self.ebox_data.async_update() From ba00c786b0855afe55b0b6c72b57b8398689e3b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Jul 2021 10:45:07 -0700 Subject: [PATCH 474/818] Correctly detect is not home (#53279) --- .../components/device_tracker/device_condition.py | 12 ++++++------ .../device_tracker/test_device_condition.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 714d6d7f016..afa899444f6 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_TYPE, STATE_HOME, - STATE_NOT_HOME, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry @@ -62,14 +61,15 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] == "is_home": - state = STATE_HOME - else: - state = STATE_NOT_HOME + + reverse = config[CONF_TYPE] == "is_not_home" @callback def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" - return condition.state(hass, config[ATTR_ENTITY_ID], state) + result = condition.state(hass, config[ATTR_ENTITY_ID], STATE_HOME) + if reverse: + result = not result + return result return test_is_state diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 2cd4aceeb07..7e3f79712c4 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -3,7 +3,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.device_tracker import DOMAIN -from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.const import STATE_HOME from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -119,7 +119,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_home - event - test_event1" - hass.states.async_set("device_tracker.entity", STATE_NOT_HOME) + hass.states.async_set("device_tracker.entity", "school") hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() From fd2f15b7c783f1006219e3c10aaee229924a60ba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 21 Jul 2021 20:14:47 +0200 Subject: [PATCH 475/818] Add new unit constants (#53258) * Add new unit constant - MHz * Add new unit constants - precipitation (in, in/h) --- .../components/ambient_station/__init__.py | 23 ++++++++++++------- homeassistant/components/arwn/sensor.py | 7 +++++- homeassistant/components/huawei_lte/sensor.py | 5 ++-- homeassistant/const.py | 3 +++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 12f534eb8e9..d719f9b3728 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -28,6 +28,8 @@ from homeassistant.const import ( IRRADIATION_WATTS_PER_SQUARE_METER, LIGHT_LUX, PERCENTAGE, + PRECIPITATION_INCHES, + PRECIPITATION_INCHES_PER_HOUR, PRESSURE_INHG, SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, @@ -156,7 +158,7 @@ TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m" TYPE_WINDSPEEDMPH = "windspeedmph" TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_TYPES = { - TYPE_24HOURRAININ: ("24 Hr Rain", "in", SENSOR, None), + TYPE_24HOURRAININ: ("24 Hr Rain", PRECIPITATION_INCHES, SENSOR, None), TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE), TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE), TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), @@ -172,11 +174,16 @@ SENSOR_TYPES = { TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2), - TYPE_DAILYRAININ: ("Daily Rain", "in", SENSOR, None), + TYPE_DAILYRAININ: ("Daily Rain", PRECIPITATION_INCHES, SENSOR, None), TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), - TYPE_EVENTRAININ: ("Event Rain", "in", SENSOR, None), + TYPE_EVENTRAININ: ("Event Rain", PRECIPITATION_INCHES, SENSOR, None), TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), - TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", SENSOR, None), + TYPE_HOURLYRAININ: ( + "Hourly Rain Rate", + PRECIPITATION_INCHES_PER_HOUR, + SENSOR, + None, + ), TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), @@ -191,7 +198,7 @@ SENSOR_TYPES = { TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP), TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None), - TYPE_MONTHLYRAININ: ("Monthly Rain", "in", SENSOR, None), + TYPE_MONTHLYRAININ: ("Monthly Rain", PRECIPITATION_INCHES, SENSOR, None), TYPE_PM25_24H: ( "PM25 24h Avg", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -277,9 +284,9 @@ SENSOR_TYPES = { TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), - TYPE_TOTALRAININ: ("Lifetime Rain", "in", SENSOR, None), + TYPE_TOTALRAININ: ("Lifetime Rain", PRECIPITATION_INCHES, SENSOR, None), TYPE_UV: ("uv", "Index", SENSOR, None), - TYPE_WEEKLYRAININ: ("Weekly Rain", "in", SENSOR, None), + TYPE_WEEKLYRAININ: ("Weekly Rain", PRECIPITATION_INCHES, SENSOR, None), TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None), TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None), TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None), @@ -288,7 +295,7 @@ SENSOR_TYPES = { TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None), TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None), TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None), - TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None), + TYPE_YEARLYRAININ: ("Yearly Rain", PRECIPITATION_INCHES, SENSOR, None), } CONFIG_SCHEMA = cv.deprecated(DOMAIN) diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 1c95911a19e..2300319f9a4 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEGREE, DEVICE_CLASS_TEMPERATURE, + PRECIPITATION_INCHES, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -44,7 +45,11 @@ def discover_sensors(topic, payload): if domain == "rain": if len(parts) >= 3 and parts[2] == "today": return ArwnSensor( - topic, "Rain Since Midnight", "since_midnight", "in", "mdi:water" + topic, + "Rain Since Midnight", + "since_midnight", + PRECIPITATION_INCHES, + "mdi:water", ) return ( ArwnSensor(topic + "/total", "Total Rainfall", "total", unit, "mdi:water"), diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 6554e69d76e..4340d5912c9 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, DATA_RATE_BYTES_PER_SECOND, + FREQUENCY_MEGAHERTZ, PERCENTAGE, STATE_UNKNOWN, TIME_SECONDS, @@ -192,11 +193,11 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( name="Downlink frequency", - formatter=lambda x: (round(int(x) / 10), "MHz"), + formatter=lambda x: (round(int(x) / 10), FREQUENCY_MEGAHERTZ), ), (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( name="Uplink frequency", - formatter=lambda x: (round(int(x) / 10), "MHz"), + formatter=lambda x: (round(int(x) / 10), FREQUENCY_MEGAHERTZ), ), KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( diff --git a/homeassistant/const.py b/homeassistant/const.py index 13b94b799db..0b8523bfa6f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -452,6 +452,7 @@ LENGTH_MILES: Final = "mi" # Frequency units FREQUENCY_HERTZ: Final = "Hz" +FREQUENCY_MEGAHERTZ: Final = "MHz" FREQUENCY_GIGAHERTZ: Final = "GHz" # Pressure units @@ -509,6 +510,8 @@ IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)" # Precipitation units PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" +PRECIPITATION_INCHES: Final = "in" +PRECIPITATION_INCHES_PER_HOUR: Final = "in/h" # Concentration units CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" From 3eb3c2824c409feb9e2c68859fa6c04dd2574a7c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 21 Jul 2021 14:52:17 -0400 Subject: [PATCH 476/818] Refactor goalzero (#53282) --- .../components/goalzero/binary_sensor.py | 28 +++++-------------- homeassistant/components/goalzero/const.py | 22 +++++++++------ homeassistant/components/goalzero/sensor.py | 8 +++--- homeassistant/components/goalzero/switch.py | 13 ++------- 4 files changed, 27 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 74776eb51b5..f9a110eff55 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Goal Zero Yeti Sensors.""" from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, CONF_NAME from . import YetiEntity from .const import BINARY_SENSOR_DICT, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN @@ -39,21 +39,12 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): super().__init__(api, coordinator, name, server_unique_id) self._condition = sensor_name - - variable_info = BINARY_SENSOR_DICT[sensor_name] - self._condition_name = variable_info[0] - self._icon = variable_info[2] - self._device_class = variable_info[1] - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._name} {self._condition_name}" - - @property - def unique_id(self) -> str: - """Return the unique id of the sensor.""" - return f"{self._server_unique_id}/{self._condition_name}" + self._attr_device_class = BINARY_SENSOR_DICT[sensor_name].get(ATTR_DEVICE_CLASS) + self._attr_icon = BINARY_SENSOR_DICT[sensor_name].get(ATTR_ICON) + self._attr_name = f"{name} {BINARY_SENSOR_DICT[sensor_name].get(ATTR_NAME)}" + self._attr_unique_id = ( + f"{server_unique_id}/{BINARY_SENSOR_DICT[sensor_name].get(ATTR_NAME)}" + ) @property def is_on(self) -> bool: @@ -61,8 +52,3 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): if self.api.data: return self.api.data[self._condition] == 1 return False - - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return self._icon diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index da4a6ee4ad6..e9fed7dc52b 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -18,6 +18,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, ELECTRIC_CURRENT_AMPERE, @@ -42,14 +43,19 @@ DATA_KEY_API = "api" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) BINARY_SENSOR_DICT = { - "backlight": ["Backlight", None, "mdi:clock-digital"], - "app_online": [ - "App Online", - DEVICE_CLASS_CONNECTIVITY, - None, - ], - "isCharging": ["Charging", DEVICE_CLASS_BATTERY_CHARGING, None], - "inputDetected": ["Input Detected", DEVICE_CLASS_POWER, None], + "backlight": {ATTR_NAME: "Backlight", ATTR_ICON: "mdi:clock-digital"}, + "app_online": { + ATTR_NAME: "App Online", + ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, + }, + "isCharging": { + ATTR_NAME: "Charging", + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + }, + "inputDetected": { + ATTR_NAME: "Input Detected", + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + }, } SENSOR_DICT = { diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index f64d6d772c8..594e1f0046b 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -44,13 +44,13 @@ class YetiSensor(YetiEntity): super().__init__(api, coordinator, name, server_unique_id) self._condition = sensor_name sensor = SENSOR_DICT[sensor_name] - self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" - self._attr_unique_id = f"{self._server_unique_id}/{sensor_name}" - self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) - self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) + self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) self._attr_last_reset = sensor.get(ATTR_LAST_RESET) + self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" self._attr_state_class = sensor.get(ATTR_STATE_CLASS) + self._attr_unique_id = f"{server_unique_id}/{sensor_name}" + self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) @property def state(self) -> str | None: diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 92808ef5f43..9d37bcb0b7b 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -38,17 +38,8 @@ class YetiSwitch(YetiEntity, SwitchEntity): """Initialize a Goal Zero Yeti switch.""" super().__init__(api, coordinator, name, server_unique_id) self._condition = switch_name - self._condition_name = SWITCH_DICT[switch_name] - - @property - def name(self) -> str: - """Return the name of the switch.""" - return f"{self._name} {self._condition_name}" - - @property - def unique_id(self) -> str: - """Return the unique id of the switch.""" - return f"{self._server_unique_id}/{self._condition}" + self._attr_name = f"{name} {SWITCH_DICT[switch_name]}" + self._attr_unique_id = f"{server_unique_id}/{switch_name}" @property def is_on(self) -> bool: From 6636e5b7372dc216d7802385f5519f775d656f17 Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 21 Jul 2021 21:35:44 +0200 Subject: [PATCH 477/818] Flipr integration (#46582) Co-authored-by: Franck Nijhof Co-authored-by: cnico <> --- CODEOWNERS | 1 + homeassistant/components/flipr/__init__.py | 90 ++++++++++ homeassistant/components/flipr/config_flow.py | 124 +++++++++++++ homeassistant/components/flipr/const.py | 10 ++ homeassistant/components/flipr/manifest.json | 12 ++ homeassistant/components/flipr/sensor.py | 90 ++++++++++ homeassistant/components/flipr/strings.json | 30 ++++ .../components/flipr/translations/en.json | 30 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/flipr/__init__.py | 1 + tests/components/flipr/test_config_flow.py | 166 ++++++++++++++++++ tests/components/flipr/test_init.py | 28 +++ tests/components/flipr/test_sensors.py | 92 ++++++++++ 15 files changed, 681 insertions(+) create mode 100644 homeassistant/components/flipr/__init__.py create mode 100644 homeassistant/components/flipr/config_flow.py create mode 100644 homeassistant/components/flipr/const.py create mode 100644 homeassistant/components/flipr/manifest.json create mode 100644 homeassistant/components/flipr/sensor.py create mode 100644 homeassistant/components/flipr/strings.json create mode 100644 homeassistant/components/flipr/translations/en.json create mode 100644 tests/components/flipr/__init__.py create mode 100644 tests/components/flipr/test_config_flow.py create mode 100644 tests/components/flipr/test_init.py create mode 100644 tests/components/flipr/test_sensors.py diff --git a/CODEOWNERS b/CODEOWNERS index 5fa884fda15..28dc16e7342 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -161,6 +161,7 @@ homeassistant/components/fireservicerota/* @cyberjunky homeassistant/components/firmata/* @DaAwesomeP homeassistant/components/fixer/* @fabaff homeassistant/components/flick_electric/* @ZephireNZ +homeassistant/components/flipr/* @cnico homeassistant/components/flo/* @dmulcahey homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py new file mode 100644 index 00000000000..05bbd0d5449 --- /dev/null +++ b/homeassistant/components/flipr/__init__.py @@ -0,0 +1,90 @@ +"""The Flipr integration.""" +from datetime import timedelta +import logging + +from flipr_api import FliprAPIRestClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_FLIPR_ID, DOMAIN, MANUFACTURER, NAME + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=60) + + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Flipr from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + coordinator = FliprDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class FliprDataUpdateCoordinator(DataUpdateCoordinator): + """Class to hold Flipr data retrieval.""" + + def __init__(self, hass, entry): + """Initialize.""" + username = entry.data[CONF_EMAIL] + password = entry.data[CONF_PASSWORD] + self.flipr_id = entry.data[CONF_FLIPR_ID] + + _LOGGER.debug("Config entry values : %s, %s", username, self.flipr_id) + + # Establishes the connection. + self.client = FliprAPIRestClient(username, password) + self.entry = entry + + super().__init__( + hass, + _LOGGER, + name=f"Flipr data measure for {self.flipr_id}", + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Fetch data from API endpoint.""" + return await self.hass.async_add_executor_job( + self.client.get_pool_measure_latest, self.flipr_id + ) + + +class FliprEntity(CoordinatorEntity): + """Implements a common class elements representing the Flipr component.""" + + def __init__(self, coordinator, flipr_id, info_type): + """Initialize Flipr sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{flipr_id}-{info_type}" + self._attr_device_info = { + "identifiers": {(DOMAIN, flipr_id)}, + "name": NAME, + "manufacturer": MANUFACTURER, + } + self.info_type = info_type + self.flipr_id = flipr_id diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py new file mode 100644 index 00000000000..b503281fed4 --- /dev/null +++ b/homeassistant/components/flipr/config_flow.py @@ -0,0 +1,124 @@ +"""Config flow for Flipr integration.""" +from __future__ import annotations + +import logging + +from flipr_api import FliprAPIRestClient +from requests.exceptions import HTTPError, Timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from .const import CONF_FLIPR_ID, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Flipr.""" + + VERSION = 1 + + _username: str | None = None + _password: str | None = None + _flipr_id: str | None = None + _possible_flipr_ids: list[str] | None = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self._show_setup_form() + + self._username = user_input[CONF_EMAIL] + self._password = user_input[CONF_PASSWORD] + + errors = {} + if not self._flipr_id: + try: + flipr_ids = await self._authenticate_and_search_flipr() + except HTTPError: + errors["base"] = "invalid_auth" + except (Timeout, ConnectionError): + errors["base"] = "cannot_connect" + except Exception as exception: # pylint: disable=broad-except + errors["base"] = "unknown" + _LOGGER.exception(exception) + + if not errors and len(flipr_ids) == 0: + # No flipr_id found. Tell the user with an error message. + errors["base"] = "no_flipr_id_found" + + if errors: + return self._show_setup_form(errors) + + if len(flipr_ids) == 1: + self._flipr_id = flipr_ids[0] + else: + # If multiple flipr found (rare case), we ask the user to choose one in a select box. + # The user will have to run config_flow as many times as many fliprs he has. + self._possible_flipr_ids = flipr_ids + return await self.async_step_flipr_id() + + # Check if already configured + await self.async_set_unique_id(self._flipr_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self._flipr_id, + data={ + CONF_EMAIL: self._username, + CONF_PASSWORD: self._password, + CONF_FLIPR_ID: self._flipr_id, + }, + ) + + def _show_setup_form(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} + ), + errors=errors, + ) + + async def _authenticate_and_search_flipr(self) -> list[str]: + """Validate the username and password provided and searches for a flipr id.""" + client = await self.hass.async_add_executor_job( + FliprAPIRestClient, self._username, self._password + ) + + flipr_ids = await self.hass.async_add_executor_job(client.search_flipr_ids) + + return flipr_ids + + async def async_step_flipr_id(self, user_input=None): + """Handle the initial step.""" + if not user_input: + # Creation of a select with the proposal of flipr ids values found by API. + flipr_ids_for_form = {} + for flipr_id in self._possible_flipr_ids: + flipr_ids_for_form[flipr_id] = f"{flipr_id}" + + return self.async_show_form( + step_id="flipr_id", + data_schema=vol.Schema( + { + vol.Required(CONF_FLIPR_ID): vol.All( + vol.Coerce(str), vol.In(flipr_ids_for_form) + ) + } + ), + ) + + # Get chosen flipr_id. + self._flipr_id = user_input[CONF_FLIPR_ID] + + return await self.async_step_user( + { + CONF_EMAIL: self._username, + CONF_PASSWORD: self._password, + CONF_FLIPR_ID: self._flipr_id, + } + ) diff --git a/homeassistant/components/flipr/const.py b/homeassistant/components/flipr/const.py new file mode 100644 index 00000000000..d28353f4776 --- /dev/null +++ b/homeassistant/components/flipr/const.py @@ -0,0 +1,10 @@ +"""Constants for the Flipr integration.""" + +DOMAIN = "flipr" + +CONF_FLIPR_ID = "flipr_id" + +ATTRIBUTION = "Flipr Data" + +MANUFACTURER = "CTAC-TECH" +NAME = "Flipr" diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json new file mode 100644 index 00000000000..330fea7de8b --- /dev/null +++ b/homeassistant/components/flipr/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "flipr", + "name": "Flipr", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/flipr", + "requirements": [ + "flipr-api==1.4.1"], + "codeowners": [ + "@cnico" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py new file mode 100644 index 00000000000..427a668a72b --- /dev/null +++ b/homeassistant/components/flipr/sensor.py @@ -0,0 +1,90 @@ +"""Sensor platform for the Flipr's pool_sensor.""" +from datetime import datetime + +from homeassistant.const import ( + ATTR_ATTRIBUTION, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + TEMP_CELSIUS, +) +from homeassistant.helpers.entity import Entity + +from . import FliprEntity +from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN + +SENSORS = { + "chlorine": { + "unit": "mV", + "icon": "mdi:pool", + "name": "Chlorine", + "device_class": None, + }, + "ph": {"unit": None, "icon": "mdi:pool", "name": "pH", "device_class": None}, + "temperature": { + "unit": TEMP_CELSIUS, + "icon": None, + "name": "Water Temp", + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "date_time": { + "unit": None, + "icon": None, + "name": "Last Measured", + "device_class": DEVICE_CLASS_TIMESTAMP, + }, + "red_ox": { + "unit": "mV", + "icon": "mdi:pool", + "name": "Red OX", + "device_class": None, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + flipr_id = config_entry.data[CONF_FLIPR_ID] + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + sensors_list = [] + for sensor in SENSORS: + sensors_list.append(FliprSensor(coordinator, flipr_id, sensor)) + + async_add_entities(sensors_list, True) + + +class FliprSensor(FliprEntity, Entity): + """Sensor representing FliprSensor data.""" + + @property + def name(self): + """Return the name of the particular component.""" + return f"Flipr {self.flipr_id} {SENSORS[self.info_type]['name']}" + + @property + def state(self): + """State of the sensor.""" + state = self.coordinator.data[self.info_type] + if isinstance(state, datetime): + return state.isoformat() + return state + + @property + def device_class(self): + """Return the device class.""" + return SENSORS[self.info_type]["device_class"] + + @property + def icon(self): + """Return the icon.""" + return SENSORS[self.info_type]["icon"] + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return SENSORS[self.info_type]["unit"] + + @property + def device_state_attributes(self): + """Return device attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/flipr/strings.json b/homeassistant/components/flipr/strings.json new file mode 100644 index 00000000000..55feaa691f7 --- /dev/null +++ b/homeassistant/components/flipr/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to Flipr", + "description": "Connect using your Flipr account.", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "flipr_id": { + "title": "Choose your Flipr", + "description": "Choose your Flipr ID in the list", + "data": { + "flipr_id": "Flipr ID" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_flipr_id_found": "No flipr id associated to your account for now. You should verify it is working with the Flipr's mobile app first." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/flipr/translations/en.json b/homeassistant/components/flipr/translations/en.json new file mode 100644 index 00000000000..017514e147c --- /dev/null +++ b/homeassistant/components/flipr/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "This Flipr is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "no_flipr_id_found": "No flipr id associated to your account for now. You should verify it is working with the Flipr's mobile app first." + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + }, + "description": "Connect to your flipr account", + "title": "Flipr device" + }, + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Choose your flipr ID in the list", + "title": "Flipr device" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 943ca9cda74..b88d5639783 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -77,6 +77,7 @@ FLOWS = [ "faa_delays", "fireservicerota", "flick_electric", + "flipr", "flo", "flume", "flunearyou", diff --git a/requirements_all.txt b/requirements_all.txt index bb119c6a2c9..8bc57855b5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -617,6 +617,9 @@ fitbit==0.3.1 # homeassistant.components.fixer fixerio==1.0.0a0 +# homeassistant.components.flipr +flipr-api==1.4.1 + # homeassistant.components.flux_led flux_led==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22a017442eb..de3bbfd46f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,6 +338,9 @@ faadelays==0.0.7 # homeassistant.components.feedreader feedparser==6.0.2 +# homeassistant.components.flipr +flipr-api==1.4.1 + # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flipr/__init__.py b/tests/components/flipr/__init__.py new file mode 100644 index 00000000000..26767261866 --- /dev/null +++ b/tests/components/flipr/__init__.py @@ -0,0 +1 @@ +"""Tests for the Flipr integration.""" diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py new file mode 100644 index 00000000000..66410938aab --- /dev/null +++ b/tests/components/flipr/test_config_flow.py @@ -0,0 +1,166 @@ +"""Test the Flipr config flow.""" +from unittest.mock import patch + +import pytest +from requests.exceptions import HTTPError, Timeout + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + + +@pytest.fixture(name="mock_setup") +def mock_setups(): + """Prevent setup.""" + with patch( + "homeassistant.components.flipr.async_setup_entry", + return_value=True, + ): + yield + + +async def test_show_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + +async def test_invalid_credential(hass, mock_setup): + """Test invalid credential.""" + with patch( + "flipr_api.FliprAPIRestClient.search_flipr_ids", side_effect=HTTPError() + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_EMAIL: "bad_login", + CONF_PASSWORD: "bad_pass", + CONF_FLIPR_ID: "", + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_nominal_case(hass, mock_setup): + """Test valid login form.""" + with patch( + "flipr_api.FliprAPIRestClient.search_flipr_ids", + return_value=["flipid"], + ) as mock_flipr_client: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_EMAIL: "dummylogin", + CONF_PASSWORD: "dummypass", + CONF_FLIPR_ID: "flipid", + }, + ) + await hass.async_block_till_done() + + assert len(mock_flipr_client.mock_calls) == 1 + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "flipid" + assert result["data"] == { + CONF_EMAIL: "dummylogin", + CONF_PASSWORD: "dummypass", + CONF_FLIPR_ID: "flipid", + } + + +async def test_multiple_flip_id(hass, mock_setup): + """Test multiple flipr id adding a config step.""" + with patch( + "flipr_api.FliprAPIRestClient.search_flipr_ids", + return_value=["FLIP1", "FLIP2"], + ) as mock_flipr_client: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_EMAIL: "dummylogin", + CONF_PASSWORD: "dummypass", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "flipr_id" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_FLIPR_ID: "FLIP2"}, + ) + + assert len(mock_flipr_client.mock_calls) == 1 + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "FLIP2" + assert result["data"] == { + CONF_EMAIL: "dummylogin", + CONF_PASSWORD: "dummypass", + CONF_FLIPR_ID: "FLIP2", + } + + +async def test_no_flip_id(hass, mock_setup): + """Test no flipr id found.""" + with patch( + "flipr_api.FliprAPIRestClient.search_flipr_ids", + return_value=[], + ) as mock_flipr_client: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_EMAIL: "dummylogin", + CONF_PASSWORD: "dummypass", + }, + ) + + assert result["step_id"] == "user" + assert result["type"] == "form" + assert result["errors"] == {"base": "no_flipr_id_found"} + + assert len(mock_flipr_client.mock_calls) == 1 + + +async def test_http_errors(hass, mock_setup): + """Test HTTP Errors.""" + with patch("flipr_api.FliprAPIRestClient.search_flipr_ids", side_effect=Timeout()): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_EMAIL: "nada", + CONF_PASSWORD: "nada", + CONF_FLIPR_ID: "", + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + with patch( + "flipr_api.FliprAPIRestClient.search_flipr_ids", + side_effect=Exception("Bad request Boy :) --"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_EMAIL: "nada", + CONF_PASSWORD: "nada", + CONF_FLIPR_ID: "", + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_init.py b/tests/components/flipr/test_init.py new file mode 100644 index 00000000000..08487c18a46 --- /dev/null +++ b/tests/components/flipr/test_init.py @@ -0,0 +1,28 @@ +"""Tests for init methods.""" +from unittest.mock import patch + +from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant): + """Test unload entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_EMAIL: "dummylogin", + CONF_PASSWORD: "dummypass", + CONF_FLIPR_ID: "FLIP1", + }, + unique_id="123456", + ) + entry.add_to_hass(hass) + with patch("homeassistant.components.flipr.FliprAPIRestClient"): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/flipr/test_sensors.py b/tests/components/flipr/test_sensors.py new file mode 100644 index 00000000000..244ec61507c --- /dev/null +++ b/tests/components/flipr/test_sensors.py @@ -0,0 +1,92 @@ +"""Test the Flipr sensor and binary sensor.""" +from datetime import datetime +from unittest.mock import patch + +from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_EMAIL, + CONF_PASSWORD, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from tests.common import MockConfigEntry + +# Data for the mocked object returned via flipr_api client. +MOCK_DATE_TIME = datetime(2021, 2, 15, 9, 10, 32, tzinfo=dt_util.UTC) +MOCK_FLIPR_MEASURE = { + "temperature": 10.5, + "ph": 7.03, + "chlorine": 0.23654886, + "red_ox": 657.58, + "date_time": MOCK_DATE_TIME, + "ph_status": "TooLow", + "chlorine_status": "Medium", +} + + +async def test_sensors(hass: HomeAssistant) -> None: + """Test the creation and values of the Flipr sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test_entry_unique_id", + data={ + CONF_EMAIL: "toto@toto.com", + CONF_PASSWORD: "myPassword", + CONF_FLIPR_ID: "myfliprid", + }, + ) + + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for sensors + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "my_random_entity_id", + suggested_object_id="sensor.flipr_myfliprid_chlorine", + disabled_by=None, + ) + + with patch( + "flipr_api.FliprAPIRestClient.get_pool_measure_latest", + return_value=MOCK_FLIPR_MEASURE, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.flipr_myfliprid_ph") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:pool" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "7.03" + + state = hass.states.get("sensor.flipr_myfliprid_water_temp") + assert state + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is TEMP_CELSIUS + assert state.state == "10.5" + + state = hass.states.get("sensor.flipr_myfliprid_last_measured") + assert state + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "2021-02-15T09:10:32+00:00" + + state = hass.states.get("sensor.flipr_myfliprid_red_ox") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:pool" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" + assert state.state == "657.58" + + state = hass.states.get("sensor.flipr_myfliprid_chlorine") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:pool" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" + assert state.state == "0.23654886" From 8d9345c40717723484a7634ec381d8e5b0b72fa0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 21 Jul 2021 14:18:08 -0600 Subject: [PATCH 478/818] Add missing type annotations to Airvisual (#52615) --- .strict-typing | 1 + .../components/airvisual/__init__.py | 67 ++++++++++++------- .../components/airvisual/config_flow.py | 52 +++++++++----- homeassistant/components/airvisual/sensor.py | 42 ++++++++++-- mypy.ini | 11 +++ 5 files changed, 125 insertions(+), 48 deletions(-) diff --git a/.strict-typing b/.strict-typing index 735d9ca4a64..ed6062d19f7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -9,6 +9,7 @@ homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* +homeassistant.components.airvisual.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.amazon_polly.* diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 89963bff623..015c913b815 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,6 +1,10 @@ """The airvisual component.""" +from __future__ import annotations + +from collections.abc import Mapping, MutableMapping from datetime import timedelta from math import ceil +from typing import Any, Dict, cast from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -10,6 +14,7 @@ from pyairvisual.errors import ( NodeProError, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -20,7 +25,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, CONF_STATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( aiohttp_client, @@ -57,11 +62,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) @callback -def async_get_geography_id(geography_dict): +def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str: """Generate a unique ID from a geography dict.""" - if not geography_dict: - return - if CONF_CITY in geography_dict: return ", ".join( ( @@ -76,7 +78,9 @@ def async_get_geography_id(geography_dict): @callback -def async_get_cloud_api_update_interval(hass, api_key, num_consumers): +def async_get_cloud_api_update_interval( + hass: HomeAssistant, api_key: str, num_consumers: int +) -> timedelta: """Get a leveled scan interval for a particular cloud API key. This will shift based on the number of active consumers, thus keeping the user @@ -97,18 +101,22 @@ def async_get_cloud_api_update_interval(hass, api_key, num_consumers): @callback -def async_get_cloud_coordinators_by_api_key(hass, api_key): +def async_get_cloud_coordinators_by_api_key( + hass: HomeAssistant, api_key: str +) -> list[DataUpdateCoordinator]: """Get all DataUpdateCoordinator objects related to a particular API key.""" coordinators = [] for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): config_entry = hass.config_entries.async_get_entry(entry_id) - if config_entry.data.get(CONF_API_KEY) == api_key: + if config_entry and config_entry.data.get(CONF_API_KEY) == api_key: coordinators.append(coordinator) return coordinators @callback -def async_sync_geo_coordinator_update_intervals(hass, api_key): +def async_sync_geo_coordinator_update_intervals( + hass: HomeAssistant, api_key: str +) -> None: """Sync the update interval for geography-based data coordinators (by API key).""" coordinators = async_get_cloud_coordinators_by_api_key(hass, api_key) @@ -129,7 +137,9 @@ def async_sync_geo_coordinator_update_intervals(hass, api_key): @callback -def _standardize_geography_config_entry(hass, config_entry): +def _standardize_geography_config_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Ensure that geography config entries have appropriate properties.""" entry_updates = {} @@ -162,9 +172,11 @@ def _standardize_geography_config_entry(hass, config_entry): @callback -def _standardize_node_pro_config_entry(hass, config_entry): +def _standardize_node_pro_config_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Ensure that Node/Pro config entries have appropriate properties.""" - entry_updates = {} + entry_updates: dict[str, Any] = {} if CONF_INTEGRATION_TYPE not in config_entry.data: # If the config entry data doesn't contain the integration type, add it: @@ -179,7 +191,7 @@ def _standardize_node_pro_config_entry(hass, config_entry): hass.config_entries.async_update_entry(config_entry, **entry_updates) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) @@ -189,7 +201,7 @@ async def async_setup_entry(hass, config_entry): websession = aiohttp_client.async_get_clientsession(hass) cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) - async def async_update_data(): + async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" if CONF_CITY in config_entry.data: api_coro = cloud_api.air_quality.city( @@ -204,7 +216,8 @@ async def async_setup_entry(hass, config_entry): ) try: - return await api_coro + data = await api_coro + return cast(Dict[str, Any], data) except (InvalidKeyError, KeyExpiredError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: @@ -242,13 +255,14 @@ async def async_setup_entry(hass, config_entry): _standardize_node_pro_config_entry(hass, config_entry) - async def async_update_data(): + async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" try: async with NodeSamba( config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] ) as node: - return await node.async_get_latest_measurements() + data = await node.async_get_latest_measurements() + return cast(Dict[str, Any], data) except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -275,7 +289,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_migrate_entry(hass, config_entry): +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate an old config entry.""" version = config_entry.version @@ -317,7 +331,7 @@ async def async_migrate_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload an AirVisual config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS @@ -338,7 +352,7 @@ async def async_unload_entry(hass, config_entry): return unload_ok -async def async_reload_entry(hass, config_entry): +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle an options update.""" await hass.config_entries.async_reload(config_entry.entry_id) @@ -346,16 +360,19 @@ async def async_reload_entry(hass, config_entry): class AirVisualEntity(CoordinatorEntity): """Define a generic AirVisual entity.""" - def __init__(self, coordinator): + def __init__(self, coordinator: DataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - async def async_added_to_hass(self): + self._attr_extra_state_attributes: MutableMapping[str, Any] = { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION + } + + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" self.update_from_latest_data() self.async_write_ha_state() @@ -365,6 +382,6 @@ class AirVisualEntity(CoordinatorEntity): self.update_from_latest_data() @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the entity from the latest data.""" raise NotImplementedError diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index ef7873a31b1..971dee161cb 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -1,4 +1,6 @@ """Define a config flow manager for AirVisual.""" +from __future__ import annotations + import asyncio from pyairvisual import CloudAPI, NodeSamba @@ -11,6 +13,7 @@ from pyairvisual.errors import ( import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import ( CONF_API_KEY, CONF_IP_ADDRESS, @@ -21,6 +24,7 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from . import async_get_geography_id @@ -64,13 +68,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" - self._entry_data_for_reauth = None - self._geo_id = None + self._entry_data_for_reauth: dict[str, str] = {} + self._geo_id: str | None = None @property - def geography_coords_schema(self): + def geography_coords_schema(self) -> vol.Schema: """Return the data schema for the cloud API.""" return API_KEY_DATA_SCHEMA.extend( { @@ -83,7 +87,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _async_finish_geography(self, user_input, integration_type): + async def _async_finish_geography( + self, user_input: dict[str, str], integration_type: str + ) -> FlowResult: """Validate a Cloud API key.""" websession = aiohttp_client.async_get_clientsession(self.hass) cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) @@ -142,25 +148,29 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={**user_input, CONF_INTEGRATION_TYPE: integration_type}, ) - async def _async_init_geography(self, user_input, integration_type): + async def _async_init_geography( + self, user_input: dict[str, str], integration_type: str + ) -> FlowResult: """Handle the initialization of the integration via the cloud API.""" self._geo_id = async_get_geography_id(user_input) await self._async_set_unique_id(self._geo_id) self._abort_if_unique_id_configured() return await self._async_finish_geography(user_input, integration_type) - async def _async_set_unique_id(self, unique_id): + async def _async_set_unique_id(self, unique_id: str) -> None: """Set the unique ID of the config flow and abort if it already exists.""" await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Define the config flow to handle options.""" return AirVisualOptionsFlowHandler(config_entry) - async def async_step_geography_by_coords(self, user_input=None): + async def async_step_geography_by_coords( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the initialization of the cloud API based on latitude/longitude.""" if not user_input: return self.async_show_form( @@ -171,7 +181,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS ) - async def async_step_geography_by_name(self, user_input=None): + async def async_step_geography_by_name( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the initialization of the cloud API based on city/state/country.""" if not user_input: return self.async_show_form( @@ -182,7 +194,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME ) - async def async_step_node_pro(self, user_input=None): + async def async_step_node_pro( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the initialization of the integration with a Node/Pro.""" if not user_input: return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA) @@ -208,13 +222,15 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, ) - async def async_step_reauth(self, data): + async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: """Handle configuration by re-auth.""" self._entry_data_for_reauth = data self._geo_id = async_get_geography_id(data) return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -227,7 +243,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form( @@ -244,11 +262,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): """Handle an AirVisual options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index d4b988a0ddc..693742217e5 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,5 +1,8 @@ """Support for AirVisual air quality sensors.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -18,7 +21,10 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import AirVisualEntity from .const import ( @@ -141,10 +147,15 @@ POLLUTANT_UNITS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AirVisual sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor] if config_entry.data[CONF_INTEGRATION_TYPE] in [ INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, @@ -174,7 +185,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AirVisualGeographySensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to geography data via the Cloud API.""" - def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale): + def __init__( + self, + coordinator: DataUpdateCoordinator, + config_entry: ConfigEntry, + kind: str, + name: str, + icon: str, + unit: str | None, + locale: str, + ) -> None: """Initialize.""" super().__init__(coordinator) @@ -203,7 +223,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): return super().available and self.coordinator.data["current"]["pollution"] @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the entity from the latest data.""" try: data = self.coordinator.data["current"]["pollution"] @@ -260,7 +280,15 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" - def __init__(self, coordinator, kind, name, device_class, icon, unit): + def __init__( + self, + coordinator: DataUpdateCoordinator, + kind: str, + name: str, + device_class: str | None, + icon: str | None, + unit: str, + ) -> None: """Initialize.""" super().__init__(coordinator) @@ -274,7 +302,7 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): self._kind = kind @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" return { "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, @@ -288,7 +316,7 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): } @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the entity from the latest data.""" if self._kind == SENSOR_KIND_AQI: if self.coordinator.data["settings"]["is_aqi_usa"]: diff --git a/mypy.ini b/mypy.ini index 22cd7f4a1e0..67f631f05bf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -110,6 +110,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.airvisual.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aladdin_connect.*] check_untyped_defs = true disallow_incomplete_defs = true From f3d95501d90f90ac0f2ecd75afd0104a526121eb Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Wed, 21 Jul 2021 23:15:47 +0200 Subject: [PATCH 479/818] Add refresh after turning switch on or off and type annotations to ezviz (#52469) --- homeassistant/components/ezviz/switch.py | 68 ++++++++++++++++-------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 00230a3ac2d..9949dc18b23 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,26 +1,34 @@ """Support for Ezviz Switch sensors.""" +from __future__ import annotations + import logging +from typing import Any from pyezviz.constants import DeviceSwitchType +from pyezviz.exceptions import HTTPError, PyEzvizError from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .coordinator import EzvizDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Ezviz switch based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] switch_entities = [] - supported_switches = [] - - for switches in DeviceSwitchType: - supported_switches.append(switches.value) - - supported_switches = set(supported_switches) + supported_switches = {switches.value for switches in DeviceSwitchType} for idx, camera in enumerate(coordinator.data): if not camera.get("switches"): @@ -36,7 +44,11 @@ async def async_setup_entry(hass, entry, async_add_entities): class EzvizSwitch(CoordinatorEntity, SwitchEntity): """Representation of a Ezviz sensor.""" - def __init__(self, coordinator, idx, switch): + coordinator: EzvizDataUpdateCoordinator + + def __init__( + self, coordinator: EzvizDataUpdateCoordinator, idx: int, switch: str + ) -> None: """Initialize the switch.""" super().__init__(coordinator) self._idx = idx @@ -47,34 +59,48 @@ class EzvizSwitch(CoordinatorEntity, SwitchEntity): self._device_class = DEVICE_CLASS_SWITCH @property - def name(self): + def name(self) -> str: """Return the name of the Ezviz switch.""" - return f"{self._camera_name}.{DeviceSwitchType(self._name).name}" + return f"{DeviceSwitchType(self._name).name}" @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the switch.""" return self.coordinator.data[self._idx]["switches"][self._name] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this switch.""" return f"{self._serial}_{self._sensor_name}" - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Change a device switch on the camera.""" - _LOGGER.debug("Set EZVIZ Switch '%s' to on", self._name) + try: + update_ok = await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.switch_status, self._serial, self._name, 1 + ) - self.coordinator.ezviz_client.switch_status(self._serial, self._name, 1) + except (HTTPError, PyEzvizError) as err: + raise PyEzvizError("Failed to turn on switch {self._name}") from err - def turn_off(self, **kwargs): + if update_ok: + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: """Change a device switch on the camera.""" - _LOGGER.debug("Set EZVIZ Switch '%s' to off", self._name) + try: + update_ok = await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.switch_status, self._serial, self._name, 0 + ) - self.coordinator.ezviz_client.switch_status(self._serial, self._name, 0) + except (HTTPError, PyEzvizError) as err: + raise PyEzvizError(f"Failed to turn off switch {self._name}") from err + + if update_ok: + await self.coordinator.async_request_refresh() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, self._serial)}, @@ -85,6 +111,6 @@ class EzvizSwitch(CoordinatorEntity, SwitchEntity): } @property - def device_class(self): + def device_class(self) -> str: """Device class for the sensor.""" return self._device_class From cfd69de5a75efabfbe41518c9616339751d8ddb8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Jul 2021 23:28:22 +0200 Subject: [PATCH 480/818] Upgrade PyNaCl to 1.4.0 (#53287) --- homeassistant/components/mobile_app/manifest.json | 2 +- homeassistant/components/owntracks/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index d850d9ab469..a59f9bf28cf 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,7 +3,7 @@ "name": "Mobile App", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": ["PyNaCl==1.3.0", "emoji==1.2.0"], + "requirements": ["PyNaCl==1.4.0", "emoji==1.2.0"], "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@robbiet480"], diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 9e83e5b4ec4..40dbb7d569c 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,7 +3,7 @@ "name": "OwnTracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": ["PyNaCl==1.3.0"], + "requirements": ["PyNaCl==1.4.0"], "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], "codeowners": [], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 62b7c5e95d5..f39ad5e61a0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,5 @@ PyJWT==1.7.1 -PyNaCl==1.3.0 +PyNaCl==1.4.0 aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8bc57855b5c..0644dccc04b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -39,7 +39,7 @@ PyMata==2.20 # homeassistant.components.mobile_app # homeassistant.components.owntracks -PyNaCl==1.3.0 +PyNaCl==1.4.0 # homeassistant.auth.mfa_modules.totp # homeassistant.components.homekit diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de3bbfd46f7..53d5ccf8a57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ PyFlick==0.0.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks -PyNaCl==1.3.0 +PyNaCl==1.4.0 # homeassistant.auth.mfa_modules.totp # homeassistant.components.homekit From 34b1ab5f5cd7fcbd89f50ee1a7bce830f99cef99 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 21 Jul 2021 23:29:27 +0200 Subject: [PATCH 481/818] Upgrade to async-upnp-client==0.19.1 (#53288) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 87730aa1316..d11b32a6dd5 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.19.0"], + "requirements": ["async-upnp-client==0.19.1"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index faadfac5c0c..432686d9027 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.7.1", - "async-upnp-client==0.19.0" + "async-upnp-client==0.19.1" ], "dependencies": ["network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index b252f5082cb..810a53c9e28 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.19.0"], + "requirements": ["async-upnp-client==0.19.1"], "dependencies": ["ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f39ad5e61a0..19682407737 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.19.0 +async-upnp-client==0.19.1 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0644dccc04b..03bad6a7778 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -307,7 +307,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.0 +async-upnp-client==0.19.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53d5ccf8a57..e8fbf70e72c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,7 +199,7 @@ arcam-fmj==0.7.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.0 +async-upnp-client==0.19.1 # homeassistant.components.aurora auroranoaa==0.0.2 From 84c482441d5669de875598538d2a45af05a40778 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jul 2021 11:29:41 -1000 Subject: [PATCH 482/818] Use None instead of STATE_UNKNOWN in template lock (#53286) --- homeassistant/components/template/lock.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 55a568ed3c2..51431d133f7 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, STATE_LOCKED, STATE_ON, - STATE_UNKNOWN, STATE_UNLOCKED, ) from homeassistant.core import callback @@ -145,7 +144,7 @@ class TemplateLock(TemplateEntity, LockEntity): self._state = result.lower() return - self._state = STATE_UNKNOWN + self._state = None async def async_added_to_hass(self): """Register callbacks.""" From 583deada83d34acb5c23c99cb0e84f0bc030e523 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 21 Jul 2021 23:36:57 +0200 Subject: [PATCH 483/818] Add type annotations for Netatmo (#52811) --- .strict-typing | 1 + homeassistant/components/netatmo/__init__.py | 14 +-- homeassistant/components/netatmo/api.py | 6 +- homeassistant/components/netatmo/camera.py | 88 ++++++++++-------- homeassistant/components/netatmo/climate.py | 90 ++++++++++++------- .../components/netatmo/config_flow.py | 27 +++--- homeassistant/components/netatmo/const.py | 3 +- .../components/netatmo/data_handler.py | 29 +++--- .../components/netatmo/device_trigger.py | 18 ++-- homeassistant/components/netatmo/helper.py | 4 +- homeassistant/components/netatmo/light.py | 34 +++++-- .../components/netatmo/manifest.json | 2 +- .../components/netatmo/media_source.py | 13 +-- .../components/netatmo/netatmo_entity_base.py | 23 +++-- homeassistant/components/netatmo/sensor.py | 67 ++++++++------ homeassistant/components/netatmo/webhook.py | 15 ++-- mypy.ini | 14 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/mypy_config.py | 1 - tests/components/netatmo/test_select.py | 12 +-- 21 files changed, 288 insertions(+), 177 deletions(-) diff --git a/.strict-typing b/.strict-typing index ed6062d19f7..98a96f98fb0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -60,6 +60,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.mysensors.* homeassistant.components.nam.* +homeassistant.components.netatmo.* homeassistant.components.network.* homeassistant.components.no_ip.* homeassistant.components.notify.* diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index d92e50107c9..edb8837fd18 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,4 +1,6 @@ """The Netatmo integration.""" +from __future__ import annotations + import logging import secrets @@ -67,7 +69,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Netatmo component.""" hass.data[DOMAIN] = { DATA_PERSONS: {}, @@ -121,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - async def unregister_webhook(_): + async def unregister_webhook(_: None) -> None: if CONF_WEBHOOK_ID not in entry.data: return _LOGGER.debug("Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID]) @@ -138,7 +140,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID] ) - async def register_webhook(event): + async def register_webhook(_: None) -> None: if CONF_WEBHOOK_ID not in entry.data: data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} hass.config_entries.async_update_entry(entry, data=data) @@ -175,7 +177,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_handle_webhook, ) - async def handle_event(event): + async def handle_event(event: dict) -> None: """Handle webhook events.""" if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: if activation_listener is not None: @@ -219,7 +221,7 @@ async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if CONF_WEBHOOK_ID in entry.data: webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) @@ -236,7 +238,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Cleanup when entry is removed.""" if ( CONF_WEBHOOK_ID in entry.data diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 19dfdac359b..e13032dc399 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -1,4 +1,6 @@ """API for Netatmo bound to HASS OAuth.""" +from typing import cast + from aiohttp import ClientSession import pyatmo @@ -17,8 +19,8 @@ class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth): super().__init__(websession) self._oauth_session = oauth_session - async def async_get_access_token(self): + async def async_get_access_token(self) -> str: """Return a valid access token for Netatmo API.""" if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() - return self._oauth_session.token["access_token"] + return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 798156b7411..4ae40181a93 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,15 +1,20 @@ """Support for the Netatmo cameras.""" +from __future__ import annotations + import logging +from typing import Any, cast import aiohttp import pyatmo import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_CAMERA_LIGHT_MODE, @@ -31,11 +36,12 @@ from .const import ( SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, + UNKNOWN, WEBHOOK_LIGHT_MODE, WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_PUSH_TYPE, ) -from .data_handler import CAMERA_DATA_CLASS_NAME +from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -43,7 +49,9 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_QUALITY = "high" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Netatmo camera platform.""" if "access_camera" not in entry.data["token"]["scope"]: _LOGGER.info( @@ -108,12 +116,12 @@ class NetatmoCamera(NetatmoBase, Camera): def __init__( self, - data_handler, - camera_id, - camera_type, - home_id, - quality, - ): + data_handler: NetatmoDataHandler, + camera_id: str, + camera_type: str, + home_id: str, + quality: str, + ) -> None: """Set up for access to the Netatmo camera images.""" Camera.__init__(self) super().__init__(data_handler) @@ -124,17 +132,19 @@ class NetatmoCamera(NetatmoBase, Camera): self._id = camera_id self._home_id = home_id - self._device_name = self._data.get_camera(camera_id=camera_id).get("name") + self._device_name = self._data.get_camera(camera_id=camera_id).get( + "name", UNKNOWN + ) self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model = camera_type self._attr_unique_id = f"{self._id}-{self._model}" self._quality = quality - self._vpnurl = None - self._localurl = None - self._status = None - self._sd_status = None - self._alim_status = None - self._is_local = None + self._vpnurl: str | None = None + self._localurl: str | None = None + self._status: str | None = None + self._sd_status: str | None = None + self._alim_status: str | None = None + self._is_local: str | None = None self._light_state = None async def async_added_to_hass(self) -> None: @@ -153,7 +163,7 @@ class NetatmoCamera(NetatmoBase, Camera): self.hass.data[DOMAIN][DATA_CAMERAS][self._id] = self._device_name @callback - def handle_event(self, event): + def handle_event(self, event: dict) -> None: """Handle webhook events.""" data = event["data"] @@ -179,7 +189,15 @@ class NetatmoCamera(NetatmoBase, Camera): self.async_write_ha_state() return - async def async_camera_image(self): + @property + def _data(self) -> pyatmo.AsyncCameraData: + """Return data for this entity.""" + return cast( + pyatmo.AsyncCameraData, + self.data_handler.data[self._data_classes[0]["name"]], + ) + + async def async_camera_image(self) -> bytes | None: """Return a still image response from the camera.""" try: return await self._data.async_get_live_snapshot(camera_id=self._id) @@ -194,43 +212,43 @@ class NetatmoCamera(NetatmoBase, Camera): return None @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return bool(self._alim_status == "on" or self._status == "disconnected") @property - def supported_features(self): + def supported_features(self) -> int: """Return supported features.""" return SUPPORT_STREAM @property - def brand(self): + def brand(self) -> str: """Return the camera brand.""" return MANUFACTURER @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return bool(self._status == "on") @property - def is_on(self): + def is_on(self) -> bool: """Return true if on.""" return self.is_streaming - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off camera.""" await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, monitoring="off" ) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on camera.""" await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, monitoring="on" ) - async def stream_source(self): + async def stream_source(self) -> str: """Return the stream source.""" url = "{0}/live/files/{1}/index.m3u8" if self._localurl: @@ -238,12 +256,12 @@ class NetatmoCamera(NetatmoBase, Camera): return url.format(self._vpnurl, self._quality) @property - def model(self): + def model(self) -> str: """Return the camera model.""" return MODELS[self._model] @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the entity's state.""" camera = self._data.get_camera(self._id) self._vpnurl, self._localurl = self._data.camera_urls(self._id) @@ -275,7 +293,7 @@ class NetatmoCamera(NetatmoBase, Camera): } ) - def process_events(self, events): + def process_events(self, events: dict) -> dict: """Add meta data to events.""" for event in events.values(): if "video_id" not in event: @@ -290,9 +308,9 @@ class NetatmoCamera(NetatmoBase, Camera): ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" return events - async def _service_set_persons_home(self, **kwargs): + async def _service_set_persons_home(self, **kwargs: Any) -> None: """Service to change current home schedule.""" - persons = kwargs.get(ATTR_PERSONS) + persons = kwargs.get(ATTR_PERSONS, {}) person_ids = [] for person in persons: for pid, data in self._data.persons.items(): @@ -304,7 +322,7 @@ class NetatmoCamera(NetatmoBase, Camera): ) _LOGGER.debug("Set %s as at home", persons) - async def _service_set_person_away(self, **kwargs): + async def _service_set_person_away(self, **kwargs: Any) -> None: """Service to mark a person as away or set the home as empty.""" person = kwargs.get(ATTR_PERSON) person_id = None @@ -327,10 +345,10 @@ class NetatmoCamera(NetatmoBase, Camera): ) _LOGGER.debug("Set home as empty") - async def _service_set_camera_light(self, **kwargs): + async def _service_set_camera_light(self, **kwargs: Any) -> None: """Service to set light mode.""" - mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) - _LOGGER.debug("Turn %s camera light for '%s'", mode, self.name) + mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE)) + _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index c041370638c..ccc5816a28b 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast import pyatmo import voluptuous as vol @@ -19,6 +20,7 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, @@ -26,11 +28,13 @@ from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_HEATING_POWER_REQUEST, @@ -49,7 +53,11 @@ from .const import ( SERVICE_SET_SCHEDULE, SIGNAL_NAME, ) -from .data_handler import HOMEDATA_DATA_CLASS_NAME, HOMESTATUS_DATA_CLASS_NAME +from .data_handler import ( + HOMEDATA_DATA_CLASS_NAME, + HOMESTATUS_DATA_CLASS_NAME, + NetatmoDataHandler, +) from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -106,8 +114,12 @@ DEFAULT_MAX_TEMP = 30 NA_THERM = "NATherm1" NA_VALVE = "NRV" +SUGGESTED_AREA = "suggested_area" -async def async_setup_entry(hass, entry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Netatmo energy platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] @@ -163,7 +175,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class NetatmoThermostat(NetatmoBase, ClimateEntity): """Representation a Netatmo thermostat.""" - def __init__(self, data_handler, home_id, room_id): + def __init__( + self, data_handler: NetatmoDataHandler, home_id: str, room_id: str + ) -> None: """Initialize the sensor.""" ClimateEntity.__init__(self) super().__init__(data_handler) @@ -189,29 +203,29 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._home_status = self.data_handler.data[self._home_status_class] self._room_status = self._home_status.rooms[room_id] - self._room_data = self._data.rooms[home_id][room_id] + self._room_data: dict = self._data.rooms[home_id][room_id] - self._model = NA_VALVE - for module in self._room_data.get("module_ids"): + self._model: str = NA_VALVE + for module in self._room_data.get("module_ids", []): if self._home_status.thermostats.get(module): self._model = NA_THERM break self._device_name = self._data.rooms[home_id][room_id]["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" - self._current_temperature = None - self._target_temperature = None - self._preset = None - self._away = None + self._current_temperature: float | None = None + self._target_temperature: float | None = None + self._preset: str | None = None + self._away: bool | None = None self._operation_list = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] self._support_flags = SUPPORT_FLAGS - self._hvac_mode = None + self._hvac_mode: str = HVAC_MODE_AUTO self._battery_level = None - self._connected = None + self._connected: bool | None = None - self._away_temperature = None - self._hg_temperature = None - self._boilerstatus = None + self._away_temperature: float | None = None + self._hg_temperature: float | None = None + self._boilerstatus: bool | None = None self._setpoint_duration = None self._selected_schedule = None @@ -240,9 +254,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): registry = await async_get_registry(self.hass) device = registry.async_get_device({(DOMAIN, self._id)}, set()) + assert device self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id - async def handle_event(self, event): + async def handle_event(self, event: dict) -> None: """Handle webhook events.""" data = event["data"] @@ -307,22 +322,29 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return @property - def supported_features(self): + def _data(self) -> pyatmo.AsyncHomeData: + """Return data for this entity.""" + return cast( + pyatmo.AsyncHomeData, self.data_handler.data[self._data_classes[0]["name"]] + ) + + @property + def supported_features(self) -> int: """Return the list of supported features.""" return self._support_flags @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temperature @@ -332,12 +354,12 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return PRECISION_HALVES @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" return self._hvac_mode @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return self._operation_list @@ -418,7 +440,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Return a list of available preset modes.""" return SUPPORT_PRESET - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: dict) -> None: """Set new target temperature for 2 hours.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is None: @@ -429,7 +451,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the entity off.""" if self._model == NA_VALVE: await self._home_status.async_set_room_thermpoint( @@ -443,7 +465,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the entity on.""" await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME) self.async_write_ha_state() @@ -454,7 +476,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return bool(self._connected) @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the entity's state.""" self._home_status = self.data_handler.data[self._home_status_class] if self._home_status is None: @@ -487,8 +509,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if "current_temperature" not in roomstatus: return - if self._model is None: - self._model = roomstatus["module_type"] self._current_temperature = roomstatus["current_temperature"] self._target_temperature = roomstatus["target_temperature"] self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] @@ -511,7 +531,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ATTR_SELECTED_SCHEDULE ] = self._selected_schedule - def _build_room_status(self): + def _build_room_status(self) -> dict: """Construct room status.""" try: roomstatus = { @@ -570,7 +590,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {} - async def _async_service_set_schedule(self, **kwargs): + async def _async_service_set_schedule(self, **kwargs: dict) -> None: schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): @@ -592,12 +612,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info for the thermostat.""" - return {**super().device_info, "suggested_area": self._room_data["name"]} + device_info: DeviceInfo = super().device_info + device_info["suggested_area"] = self._room_data["name"] + return device_info -def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: +def get_all_home_ids(home_data: pyatmo.HomeData | None) -> list[str]: """Get all the home ids returned by NetAtmo API.""" if home_data is None: return [] diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 909255aa38e..9b7c3376076 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Netatmo.""" +from __future__ import annotations + import logging import uuid @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from .const import ( @@ -32,7 +35,9 @@ class NetatmoFlowHandler( @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: """Get the options flow for this handler.""" return NetatmoOptionsFlowHandler(config_entry) @@ -62,7 +67,7 @@ class NetatmoFlowHandler( return {"scope": " ".join(scopes)} - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow start.""" await self.async_set_unique_id(DOMAIN) @@ -81,17 +86,19 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): self.options = dict(config_entry.options) self.options.setdefault(CONF_WEATHER_AREAS, {}) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: dict | None = None) -> FlowResult: """Manage the Netatmo options.""" return await self.async_step_public_weather_areas() - async def async_step_public_weather_areas(self, user_input=None): + async def async_step_public_weather_areas( + self, user_input: dict | None = None + ) -> FlowResult: """Manage configuration of Netatmo public weather areas.""" - errors = {} + errors: dict = {} if user_input is not None: new_client = user_input.pop(CONF_NEW_AREA, None) - areas = user_input.pop(CONF_WEATHER_AREAS, None) + areas = user_input.pop(CONF_WEATHER_AREAS, []) user_input[CONF_WEATHER_AREAS] = { area: self.options[CONF_WEATHER_AREAS][area] for area in areas } @@ -110,7 +117,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_WEATHER_AREAS, default=weather_areas, - ): cv.multi_select(weather_areas), + ): cv.multi_select({wa: None for wa in weather_areas}), vol.Optional(CONF_NEW_AREA): str, } ) @@ -120,7 +127,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): errors=errors, ) - async def async_step_public_weather(self, user_input=None): + async def async_step_public_weather(self, user_input: dict) -> FlowResult: """Manage configuration of Netatmo public weather sensors.""" if user_input is not None and CONF_NEW_AREA not in user_input: self.options[CONF_WEATHER_AREAS][ @@ -181,14 +188,14 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="public_weather", data_schema=data_schema) - def _create_options_entry(self): + def _create_options_entry(self) -> FlowResult: """Update config entry options.""" return self.async_create_entry( title="Netatmo Public Weather", data=self.options ) -def fix_coordinates(user_input): +def fix_coordinates(user_input: dict) -> dict: """Fix coordinates if they don't comply with the Netatmo API.""" # Ensure coordinates have acceptable length for the Netatmo API for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW): diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 8b2fb8701da..f6806ace324 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -6,6 +6,7 @@ from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN API = "api" +UNKNOWN = "unknown" DOMAIN = "netatmo" MANUFACTURER = "Netatmo" @@ -76,7 +77,7 @@ DATA_SCHEDULES = "netatmo_schedules" NETATMO_WEBHOOK_URL = None NETATMO_EVENT = "netatmo_event" -DEFAULT_PERSON = "Unknown" +DEFAULT_PERSON = UNKNOWN DEFAULT_DISCOVERY = True DEFAULT_WEBHOOKS = False diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 5e007e8634d..128a3174b9d 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -8,6 +8,7 @@ from datetime import timedelta from itertools import islice import logging from time import time +from typing import Any import pyatmo @@ -75,11 +76,11 @@ class NetatmoDataHandler: self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] self.listeners: list[CALLBACK_TYPE] = [] self.data_classes: dict = {} - self.data = {} - self._queue = deque() + self.data: dict = {} + self._queue: deque = deque() self._webhook: bool = False - async def async_setup(self): + async def async_setup(self) -> None: """Set up the Netatmo data handler.""" async_track_time_interval( @@ -94,7 +95,7 @@ class NetatmoDataHandler: ) ) - async def async_update(self, event_time): + async def async_update(self, event_time: timedelta) -> None: """ Update device. @@ -115,17 +116,17 @@ class NetatmoDataHandler: self._queue.rotate(BATCH_SIZE) @callback - def async_force_update(self, data_class_entry): + def async_force_update(self, data_class_entry: str) -> None: """Prioritize data retrieval for given data class entry.""" self.data_classes[data_class_entry].next_scan = time() self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) - async def async_cleanup(self): + async def async_cleanup(self) -> None: """Clean up the Netatmo data handler.""" for listener in self.listeners: listener() - async def handle_event(self, event): + async def handle_event(self, event: dict) -> None: """Handle webhook events.""" if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: _LOGGER.info("%s webhook successfully registered", MANUFACTURER) @@ -139,7 +140,7 @@ class NetatmoDataHandler: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) - async def async_fetch_data(self, data_class_entry): + async def async_fetch_data(self, data_class_entry: str) -> None: """Fetch data and notify.""" if self.data[data_class_entry] is None: return @@ -163,8 +164,12 @@ class NetatmoDataHandler: update_callback() async def register_data_class( - self, data_class_name, data_class_entry, update_callback, **kwargs - ): + self, + data_class_name: str, + data_class_entry: str, + update_callback: CALLBACK_TYPE, + **kwargs: Any, + ) -> None: """Register data class.""" if data_class_entry in self.data_classes: if update_callback not in self.data_classes[data_class_entry].subscriptions: @@ -189,7 +194,9 @@ class NetatmoDataHandler: self._queue.append(self.data_classes[data_class_entry]) _LOGGER.debug("Data class %s added", data_class_entry) - async def unregister_data_class(self, data_class_entry, update_callback): + async def unregister_data_class( + self, data_class_entry: str, update_callback: CALLBACK_TYPE | None + ) -> None: """Unregister data class.""" self.data_classes[data_class_entry].subscriptions.remove(update_callback) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index b0d4e18b7c9..65bc79ee712 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -63,7 +63,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -129,10 +131,10 @@ async def async_attach_trigger( device = device_registry.async_get(config[CONF_DEVICE_ID]) if not device: - return + return lambda: None if device.model not in DEVICES: - return + return lambda: None event_config = { event_trigger.CONF_PLATFORM: "event", @@ -142,10 +144,14 @@ async def async_attach_trigger( ATTR_DEVICE_ID: config[ATTR_DEVICE_ID], }, } + # if config[CONF_TYPE] in SUBTYPES: + # event_config[event_trigger.CONF_EVENT_DATA]["data"] = { + # "mode": config[CONF_SUBTYPE] + # } if config[CONF_TYPE] in SUBTYPES: - event_config[event_trigger.CONF_EVENT_DATA]["data"] = { - "mode": config[CONF_SUBTYPE] - } + event_config.update( + {event_trigger.CONF_EVENT_DATA: {"data": {"mode": config[CONF_SUBTYPE]}}} + ) event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( diff --git a/homeassistant/components/netatmo/helper.py b/homeassistant/components/netatmo/helper.py index d9ef4d1e455..7e8f32817dd 100644 --- a/homeassistant/components/netatmo/helper.py +++ b/homeassistant/components/netatmo/helper.py @@ -1,6 +1,6 @@ """Helper for Netatmo integration.""" from dataclasses import dataclass -from uuid import uuid4 +from uuid import UUID, uuid4 @dataclass @@ -14,4 +14,4 @@ class NetatmoArea: lon_sw: float mode: str show_on_map: bool - uuid: str = uuid4() + uuid: UUID = uuid4() diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 07488ad03b5..717aace1aa2 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -1,10 +1,17 @@ """Support for the Netatmo camera lights.""" +from __future__ import annotations + import logging +from typing import cast + +import pyatmo from homeassistant.components.light import LightEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DATA_HANDLER, @@ -12,6 +19,7 @@ from .const import ( EVENT_TYPE_LIGHT_MODE, MANUFACTURER, SIGNAL_NAME, + UNKNOWN, WEBHOOK_LIGHT_MODE, WEBHOOK_PUSH_TYPE, ) @@ -21,7 +29,9 @@ from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Netatmo camera light platform.""" if "access_camera" not in entry.data["token"]["scope"]: _LOGGER.info( @@ -79,7 +89,7 @@ class NetatmoLight(NetatmoBase, LightEntity): self._id = camera_id self._home_id = home_id self._model = camera_type - self._device_name = self._data.get_camera(camera_id).get("name") + self._device_name: str = self._data.get_camera(camera_id).get("name", UNKNOWN) self._attr_name = f"{MANUFACTURER} {self._device_name}" self._is_on = False self._attr_unique_id = f"{self._id}-light" @@ -97,7 +107,7 @@ class NetatmoLight(NetatmoBase, LightEntity): ) @callback - def handle_event(self, event): + def handle_event(self, event: dict) -> None: """Handle webhook events.""" data = event["data"] @@ -114,17 +124,25 @@ class NetatmoLight(NetatmoBase, LightEntity): self.async_write_ha_state() return + @property + def _data(self) -> pyatmo.AsyncCameraData: + """Return data for this entity.""" + return cast( + pyatmo.AsyncCameraData, + self.data_handler.data[self._data_classes[0]["name"]], + ) + @property def available(self) -> bool: """If the webhook is not established, mark as unavailable.""" return bool(self.data_handler.webhook) @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: dict) -> None: """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self.name) await self._data.async_set_state( @@ -133,7 +151,7 @@ class NetatmoLight(NetatmoBase, LightEntity): floodlight="on", ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: dict) -> None: """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self.name) await self._data.async_set_state( @@ -143,6 +161,6 @@ class NetatmoLight(NetatmoBase, LightEntity): ) @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the entity's state.""" self._is_on = bool(self._data.get_light_state(self._id) == "on") diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index de7fbc36038..84ef65f3001 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==5.2.0" + "pyatmo==5.2.1" ], "after_dependencies": [ "cloud", diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 99f52d95ad4..d80225c0368 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -11,7 +11,6 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_VIDEO, ) from homeassistant.components.media_player.errors import BrowseError -from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -31,7 +30,7 @@ class IncompatibleMediaSource(MediaSourceError): """Incompatible media source attributes.""" -async def async_get_media_source(hass: HomeAssistant): +async def async_get_media_source(hass: HomeAssistant) -> NetatmoSource: """Set up Netatmo media source.""" return NetatmoSource(hass) @@ -54,7 +53,9 @@ class NetatmoSource(MediaSource): return PlayMedia(url, MIME_TYPE) async def async_browse_media( - self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES + self, + item: MediaSourceItem, + media_types: tuple[str] = ("video",), ) -> BrowseMediaSource: """Return media.""" try: @@ -65,7 +66,7 @@ class NetatmoSource(MediaSource): return self._browse_media(source, camera_id, event_id) def _browse_media( - self, source: str, camera_id: str, event_id: int + self, source: str, camera_id: str, event_id: int | None ) -> BrowseMediaSource: """Browse media.""" if camera_id and camera_id not in self.events: @@ -77,7 +78,7 @@ class NetatmoSource(MediaSource): return self._build_item_response(source, camera_id, event_id) def _build_item_response( - self, source: str, camera_id: str, event_id: int = None + self, source: str, camera_id: str, event_id: int | None = None ) -> BrowseMediaSource: if event_id and event_id in self.events[camera_id]: created = dt.datetime.fromtimestamp(event_id) @@ -148,7 +149,7 @@ class NetatmoSource(MediaSource): return media -def remove_html_tags(text): +def remove_html_tags(text: str) -> str: """Remove html tags from string.""" clean = re.compile("<.*?>") return re.sub(clean, "", text) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index eb65bc4da0f..51fc14f6f8e 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import CALLBACK_TYPE, callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DATA_DEVICE_IDS, @@ -25,12 +25,14 @@ class NetatmoBase(Entity): self._data_classes: list[dict] = [] self._listeners: list[CALLBACK_TYPE] = [] - self._device_name = None - self._id = None - self._model = None + self._device_name: str = "" + self._id: str = "" + self._model: str = "" self._attr_name = None self._attr_unique_id = None - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes: dict = { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION + } async def async_added_to_hass(self) -> None: """Entity created.""" @@ -71,7 +73,7 @@ class NetatmoBase(Entity): self.async_update_callback() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() @@ -84,17 +86,12 @@ class NetatmoBase(Entity): ) @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the entity's state.""" raise NotImplementedError @property - def _data(self): - """Return data for this entity.""" - return self.data_handler.data[self._data_classes[0]["name"]] - - @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info for the sensor.""" return { "identifiers": {(DOMAIN, self._id)}, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 46bb06149cd..6204e229108 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -2,9 +2,12 @@ from __future__ import annotations import logging -from typing import NamedTuple +from typing import NamedTuple, cast + +import pyatmo from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -24,19 +27,21 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, MANUFACTURER, SIGNAL_NAME from .data_handler import ( HOMECOACH_DATA_CLASS_NAME, PUBLICDATA_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, + NetatmoDataHandler, ) from .helper import NetatmoArea from .netatmo_entity_base import NetatmoBase @@ -267,12 +272,14 @@ BATTERY_VALUES = { PUBLIC = "public" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] platform_not_ready = True - async def find_entities(data_class_name): + async def find_entities(data_class_name: str) -> list: """Find all entities.""" all_module_infos = {} data = data_handler.data @@ -330,7 +337,7 @@ async def async_setup_entry(hass, entry, async_add_entities): device_registry = await hass.helpers.device_registry.async_get_registry() - async def add_public_entities(update=True): + async def add_public_entities(update: bool = True) -> None: """Retrieve Netatmo public weather entities.""" entities = { device.name: device.id @@ -396,7 +403,13 @@ async def async_setup_entry(hass, entry, async_add_entities): class NetatmoSensor(NetatmoBase, SensorEntity): """Implementation of a Netatmo sensor.""" - def __init__(self, data_handler, data_class_name, module_info, sensor_type): + def __init__( + self, + data_handler: NetatmoDataHandler, + data_class_name: str, + module_info: dict, + sensor_type: str, + ) -> None: """Initialize the sensor.""" super().__init__(data_handler) @@ -434,20 +447,21 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self._attr_entity_registry_enabled_default = metadata.enable_default @property - def available(self): + def _data(self) -> pyatmo.AsyncWeatherStationData: + """Return data for this entity.""" + return cast( + pyatmo.AsyncWeatherStationData, + self.data_handler.data[self._data_classes[0]["name"]], + ) + + @property + def available(self) -> bool: """Return entity availability.""" return self.state is not None @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the entity's state.""" - if self._data is None: - if self.state is None: - return - _LOGGER.warning("No data from update") - self._attr_state = None - return - data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get( self._id ) @@ -531,7 +545,7 @@ def process_battery(data: int, model: str) -> str: return "Very Low" -def process_health(health): +def process_health(health: int) -> str: """Process health index and return string for display.""" if health == 0: return "Healthy" @@ -541,11 +555,10 @@ def process_health(health): return "Fair" if health == 3: return "Poor" - if health == 4: - return "Unhealthy" + return "Unhealthy" -def process_rf(strength): +def process_rf(strength: int) -> str: """Process wifi signal strength and return string for display.""" if strength >= 90: return "Low" @@ -556,7 +569,7 @@ def process_rf(strength): return "Full" -def process_wifi(strength): +def process_wifi(strength: int) -> str: """Process wifi signal strength and return string for display.""" if strength >= 86: return "Low" @@ -570,7 +583,9 @@ def process_wifi(strength): class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Represent a single sensor in a Netatmo.""" - def __init__(self, data_handler, area, sensor_type): + def __init__( + self, data_handler: NetatmoDataHandler, area: NetatmoArea, sensor_type: str + ) -> None: """Initialize the sensor.""" super().__init__(data_handler) @@ -611,13 +626,15 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ) @property - def _data(self): - return self.data_handler.data[self._signal_name] + def _data(self) -> pyatmo.AsyncPublicData: + """Return data for this entity.""" + return cast(pyatmo.AsyncPublicData, self.data_handler.data[self._signal_name]) async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() + assert self.device_info and "name" in self.device_info self.data_handler.listeners.append( async_dispatcher_connect( self.hass, @@ -626,7 +643,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ) ) - async def async_config_update_callback(self, area): + async def async_config_update_callback(self, area: NetatmoArea) -> None: """Update the entity's config.""" if self.area == area: return @@ -661,7 +678,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ) @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the entity's state.""" data = None diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 54db95e9aa0..4f39d5fe5f5 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -1,7 +1,10 @@ """The Netatmo integration.""" import logging +from aiohttp.web import Request + from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -25,7 +28,9 @@ SUBEVENT_TYPE_MAP = { } -async def async_handle_webhook(hass, webhook_id, request): +async def async_handle_webhook( + hass: HomeAssistant, webhook_id: str, request: Request +) -> None: """Handle webhook callback.""" try: data = await request.json() @@ -47,12 +52,12 @@ async def async_handle_webhook(hass, webhook_id, request): async_evaluate_event(hass, data) -def async_evaluate_event(hass, event_data): +def async_evaluate_event(hass: HomeAssistant, event_data: dict) -> None: """Evaluate events from webhook.""" - event_type = event_data.get(ATTR_EVENT_TYPE) + event_type = event_data.get(ATTR_EVENT_TYPE, "None") if event_type == "person": - for person in event_data.get(ATTR_PERSONS): + for person in event_data.get(ATTR_PERSONS, {}): person_event_data = dict(event_data) person_event_data[ATTR_ID] = person.get(ATTR_ID) person_event_data[ATTR_NAME] = hass.data[DOMAIN][DATA_PERSONS].get( @@ -67,7 +72,7 @@ def async_evaluate_event(hass, event_data): async_send_event(hass, event_type, event_data) -def async_send_event(hass, event_type, data): +def async_send_event(hass: HomeAssistant, event_type: str, data: dict) -> None: """Send events.""" _LOGGER.debug("%s: %s", event_type, data) async_dispatcher_send( diff --git a/mypy.ini b/mypy.ini index 67f631f05bf..32fff8d5105 100644 --- a/mypy.ini +++ b/mypy.ini @@ -671,6 +671,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.netatmo.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.network.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1388,9 +1399,6 @@ ignore_errors = true [mypy-homeassistant.components.nest.legacy.*] ignore_errors = true -[mypy-homeassistant.components.netatmo.*] -ignore_errors = true - [mypy-homeassistant.components.netio.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index 03bad6a7778..50ca781cfc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1316,7 +1316,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.2.0 +pyatmo==5.2.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8fbf70e72c..88b251137ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -747,7 +747,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.2.0 +pyatmo==5.2.1 # homeassistant.components.apple_tv pyatv==0.8.1 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 7c97134397b..8dff2c8f89f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -110,7 +110,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.legacy.*", - "homeassistant.components.netatmo.*", "homeassistant.components.netio.*", "homeassistant.components.nightscout.*", "homeassistant.components.nilu.*", diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index 838b2e2d290..8be010cc802 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -16,10 +16,10 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - select_entity_livingroom = "select.netatmo_myhome" + select_entity = "select.netatmo_myhome" - assert hass.states.get(select_entity_livingroom).state == "Default" - assert hass.states.get(select_entity_livingroom).attributes[ATTR_OPTIONS] == [ + assert hass.states.get(select_entity).state == "Default" + assert hass.states.get(select_entity).attributes[ATTR_OPTIONS] == [ "Default", "Winter", ] @@ -33,7 +33,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a } await simulate_webhook(hass, webhook_id, response) - assert hass.states.get(select_entity_livingroom).state == "Winter" + assert hass.states.get(select_entity).state == "Winter" # Test setting a different schedule with patch( @@ -43,7 +43,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a SELECT_DOMAIN, SERVICE_SELECT_OPTION, { - ATTR_ENTITY_ID: select_entity_livingroom, + ATTR_ENTITY_ID: select_entity, ATTR_OPTION: "Default", }, blocking=True, @@ -62,4 +62,4 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a } await simulate_webhook(hass, webhook_id, response) - assert hass.states.get(select_entity_livingroom).state == "Default" + assert hass.states.get(select_entity).state == "Default" From 86752516eeb43bd1eecb7b35e90c58416afcb05d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Jul 2021 14:48:21 -0700 Subject: [PATCH 484/818] Add WS API to access solar forecast data (#53264) Co-authored-by: Franck Nijhof --- .../components/forecast_solar/__init__.py | 25 +++++++++++++++++-- tests/components/forecast_solar/test_init.py | 24 ++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index b00e5f1c4ce..b20a0befb96 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -5,10 +5,12 @@ from datetime import timedelta import logging from forecast_solar import ForecastSolar +import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -55,7 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + websocket_api.async_register_command(hass, ws_list_forecasts) hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -77,3 +81,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) + + +@websocket_api.websocket_command({vol.Required("type"): "forecast_solar/forecasts"}) +@callback +def ws_list_forecasts( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return a list of available forecasts.""" + forecasts = {} + + for config_entry_id, coordinator in hass.data[DOMAIN].items(): + forecasts[config_entry_id] = { + timestamp.isoformat(): val + for timestamp, val in coordinator.data.watts.items() + } + + connection.send_result(msg["id"], forecasts) diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index 719041aaf58..7544f7d352b 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -1,4 +1,5 @@ """Tests for the Forecast.Solar integration.""" +from datetime import datetime, timezone from unittest.mock import MagicMock, patch from forecast_solar import ForecastSolarConnectionError @@ -14,14 +15,37 @@ async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_forecast_solar: MagicMock, + hass_ws_client, ) -> None: """Test the Forecast.Solar configuration entry loading/unloading.""" + mock_forecast_solar.estimate.return_value.watts = { + datetime(2021, 6, 27, 13, 0, tzinfo=timezone.utc): 12, + datetime(2021, 6, 27, 14, 0, tzinfo=timezone.utc): 8, + } + mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state == ConfigEntryState.LOADED + # Test WS API set up + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "forecast_solar/forecasts", + } + ) + result = await client.receive_json() + assert result["success"] + assert result["result"] == { + mock_config_entry.entry_id: { + "2021-06-27T13:00:00+00:00": 12, + "2021-06-27T14:00:00+00:00": 8, + } + } + await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() From 9f14b2cef50c1ff3af922922e1b94b84b812d167 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 22 Jul 2021 00:04:14 +0200 Subject: [PATCH 485/818] Test KNX switch (#53289) --- tests/components/knx/README.md | 71 +++++++++++++ tests/components/knx/conftest.py | 26 ++--- tests/components/knx/test_switch.py | 150 ++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 tests/components/knx/README.md create mode 100644 tests/components/knx/test_switch.py diff --git a/tests/components/knx/README.md b/tests/components/knx/README.md new file mode 100644 index 00000000000..4b5886200c4 --- /dev/null +++ b/tests/components/knx/README.md @@ -0,0 +1,71 @@ +# Testing the KNX integration + +A KNXTestKit instance can be requested from a fixture. It provides convenience methods +to test outgoing KNX telegrams and inject incoming telegrams. +To test something add a test function requesting the `hass` and `knx` fixture and +set up the KNX integration by passing a KNX config dict to `knx.setup_integration`. + +```python +async def test_something(hass, knx): + await knx.setup_integration({ + "switch": { + "name": "test_switch", + "address": "1/2/3", + } + } + ) +``` + +## Asserting outgoing telegrams + +All outgoing telegrams are pushed to an assertion queue. Assert them in order they were sent. + +- `knx.assert_no_telegram` + Asserts that no telegram was sent (assertion queue is empty). +- `knx.assert_telegram_count(count: int)` + Asserts that `count` telegrams were sent. +- `knx.assert_read(group_address: str)` + Asserts that a GroupValueRead telegram was sent to `group_address`. + The telegram will be removed from the assertion queue. +- `knx.assert_response(group_address: str, payload: int | tuple[int, ...])` + Asserts that a GroupValueResponse telegram with `payload` was sent to `group_address`. + The telegram will be removed from the assertion queue. +- `knx.assert_write(group_address: str, payload: int | tuple[int, ...])` + Asserts that a GroupValueWrite telegram with `payload` was sent to `group_address`. + The telegram will be removed from the assertion queue. + +Change some states or call some services and assert outgoing telegrams. + +```python + # turn on switch + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.test_switch"}, blocking=True + ) + # assert ON telegram + await knx.assert_write("1/2/3", True) +``` + +## Injecting incoming telegrams + +- `knx.receive_read(group_address: str)` + Inject and process a GroupValueRead telegram addressed to `group_address`. +- `knx.receive_response(group_address: str, payload: int | tuple[int, ...])` + Inject and process a GroupValueResponse telegram addressed to `group_address` containing `payload`. +- `knx.receive_write(group_address: str, payload: int | tuple[int, ...])` + Inject and process a GroupValueWrite telegram addressed to `group_address` containing `payload`. + +Receive some telegrams and assert state. + +```python + # receive OFF telegram + await knx.receive_write("1/2/3", False) + # assert OFF state + state = hass.states.get("switch.test_switch") + assert state.state is STATE_OFF +``` + +## Notes + +- For `payload` in `assert_*` and `receive_*` use `int` for DPT 1, 2 and 3 payload values (DPTBinary) and `tuple` for other DPTs (DPTArray). +- `await self.hass.async_block_till_done()` is called before `KNXTestKit.assert_*` and after `KNXTestKit.receive_*` so you don't have to explicitly call it. +- Make sure to assert every outgoing telegram that was created in a test. `assert_no_telegram` is automatically called on teardown. diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 548e620813a..26f8f1eabac 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -41,6 +41,8 @@ class KNXTestKit: def fish_xknx(*args, **kwargs): """Get the XKNX object from the constructor call.""" self.xknx = args[0] + # disable rate limiter for tests (before StateUpdater starts) + self.xknx.rate_limit = 0 return DEFAULT with patch( @@ -50,8 +52,6 @@ class KNXTestKit: ): await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) await self.hass.async_block_till_done() - # disable rate limiter for tests - self.xknx.rate_limit = 0 ######################## # Telegram counter tests @@ -101,14 +101,14 @@ class KNXTestKit: f" {group_address} - {payload}" ) - assert ( - str(telegram.destination_address) == group_address - ), f"Group address mismatch in {telegram} - Expected: {group_address}" - assert isinstance( telegram.payload, apci_type ), f"APCI type mismatch in {telegram} - Expected: {apci_type.__name__}" + assert ( + str(telegram.destination_address) == group_address + ), f"Group address mismatch in {telegram} - Expected: {group_address}" + if payload is not None: assert ( telegram.payload.value.value == payload # type: ignore @@ -134,6 +134,13 @@ class KNXTestKit: # Incoming telegrams #################### + @staticmethod + def _payload_value(payload: int | tuple[int, ...]) -> DPTArray | DPTBinary: + """Prepare payload value for GroupValueWrite or GroupValueResponse.""" + if isinstance(payload, int): + return DPTBinary(payload) + return DPTArray(payload) + async def _receive_telegram(self, group_address: str, payload: APCI) -> None: """Inject incoming KNX telegram.""" self.xknx.telegrams.put_nowait( @@ -146,13 +153,6 @@ class KNXTestKit: ) await self.hass.async_block_till_done() - @staticmethod - def _payload_value(payload: int | tuple[int, ...]) -> DPTArray | DPTBinary: - """Prepare payload value for GroupValueWrite or GroupValueResponse.""" - if isinstance(payload, int): - return DPTBinary(payload) - return DPTArray(payload) - async def receive_read( self, group_address: str, diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py new file mode 100644 index 00000000000..407d6d83267 --- /dev/null +++ b/tests/components/knx/test_switch.py @@ -0,0 +1,150 @@ +"""Test KNX switch.""" +from unittest.mock import patch + +from homeassistant.components.knx.const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + KNX_ADDRESS, +) +from homeassistant.components.knx.schema import SwitchSchema +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import State + + +async def test_switch_simple(hass, knx): + """Test simple KNX switch.""" + await knx.setup_integration( + { + SwitchSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: "1/2/3", + } + } + ) + assert len(hass.states.async_all()) == 1 + + # turn on switch + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.test"}, blocking=True + ) + await knx.assert_write("1/2/3", True) + + # turn off switch + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.test"}, blocking=True + ) + await knx.assert_write("1/2/3", False) + + # receive ON telegram + await knx.receive_write("1/2/3", True) + state = hass.states.get("switch.test") + assert state.state is STATE_ON + + # receive OFF telegram + await knx.receive_write("1/2/3", False) + state = hass.states.get("switch.test") + assert state.state is STATE_OFF + + # switch does not respond to read by default + await knx.receive_read("1/2/3") + await knx.assert_telegram_count(0) + + +async def test_switch_state(hass, knx): + """Test KNX switch with state_address.""" + _ADDRESS = "1/1/1" + _STATE_ADDRESS = "2/2/2" + + await knx.setup_integration( + { + SwitchSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: _ADDRESS, + CONF_STATE_ADDRESS: _STATE_ADDRESS, + }, + } + ) + assert len(hass.states.async_all()) == 1 + + # StateUpdater initialize state + await knx.assert_read(_STATE_ADDRESS) + await knx.receive_response(_STATE_ADDRESS, True) + state = hass.states.get("switch.test") + assert state.state is STATE_ON + + # receive OFF telegram at `address` + await knx.receive_write(_ADDRESS, False) + state = hass.states.get("switch.test") + assert state.state is STATE_OFF + + # receive ON telegram at `address` + await knx.receive_write(_ADDRESS, True) + state = hass.states.get("switch.test") + assert state.state is STATE_ON + + # receive OFF telegram at `state_address` + await knx.receive_write(_STATE_ADDRESS, False) + state = hass.states.get("switch.test") + assert state.state is STATE_OFF + + # receive ON telegram at `state_address` + await knx.receive_write(_STATE_ADDRESS, True) + state = hass.states.get("switch.test") + assert state.state is STATE_ON + + # turn off switch + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.test"}, blocking=True + ) + await knx.assert_write(_ADDRESS, False) + + # turn on switch + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.test"}, blocking=True + ) + await knx.assert_write(_ADDRESS, True) + + # switch does not respond to read by default + await knx.receive_read(_ADDRESS) + await knx.assert_telegram_count(0) + + +async def test_switch_restore_and_respond(hass, knx): + """Test restoring KNX switch state and respond to read.""" + _ADDRESS = "1/1/1" + fake_state = State("switch.test", "on") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + await knx.setup_integration( + { + SwitchSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: _ADDRESS, + CONF_RESPOND_TO_READ: True, + }, + } + ) + + # restored state - doesn't send telegram + state = hass.states.get("switch.test") + assert state.state == STATE_ON + await knx.assert_telegram_count(0) + + # respond to restored state + await knx.receive_read(_ADDRESS) + await knx.assert_response(_ADDRESS, True) + + # turn off switch + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.test"}, blocking=True + ) + await knx.assert_write(_ADDRESS, False) + state = hass.states.get("switch.test") + assert state.state == STATE_OFF + + # respond to new state + await knx.receive_read(_ADDRESS) + await knx.assert_response(_ADDRESS, False) From edf42bab25ae2e3681f3d0e774b7eebdaaa87ce3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Jul 2021 15:04:30 -0700 Subject: [PATCH 486/818] Migrate forecast solar to v2 (#53259) --- .../components/forecast_solar/const.py | 18 +++++++++ .../components/forecast_solar/manifest.json | 2 +- .../components/forecast_solar/models.py | 4 ++ .../components/forecast_solar/sensor.py | 8 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/forecast_solar/conftest.py | 39 ++++++++++++------- .../components/forecast_solar/test_sensor.py | 16 ++++---- 8 files changed, 64 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 12aa1ee5362..7372ac5954d 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -1,6 +1,7 @@ """Constants for the Forecast.Solar integration.""" from __future__ import annotations +from datetime import timedelta from typing import Final from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT @@ -27,12 +28,14 @@ SENSORS: list[ForecastSolarSensor] = [ ForecastSolarSensor( key="energy_production_today", name="Estimated Energy Production - Today", + state=lambda estimate: estimate.energy_production_today / 1000, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensor( key="energy_production_tomorrow", name="Estimated Energy Production - Tomorrow", + state=lambda estimate: estimate.energy_production_tomorrow / 1000, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), @@ -50,11 +53,16 @@ SENSORS: list[ForecastSolarSensor] = [ key="power_production_now", name="Estimated Power Production - Now", device_class=DEVICE_CLASS_POWER, + state=lambda estimate: estimate.power_production_now / 1000, state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), ForecastSolarSensor( key="power_production_next_hour", + state=lambda estimate: estimate.power_production_at_time( + estimate.now() + timedelta(hours=1) + ) + / 1000, name="Estimated Power Production - Next Hour", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -62,6 +70,10 @@ SENSORS: list[ForecastSolarSensor] = [ ), ForecastSolarSensor( key="power_production_next_12hours", + state=lambda estimate: estimate.power_production_at_time( + estimate.now() + timedelta(hours=12) + ) + / 1000, name="Estimated Power Production - Next 12 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -69,6 +81,10 @@ SENSORS: list[ForecastSolarSensor] = [ ), ForecastSolarSensor( key="power_production_next_24hours", + state=lambda estimate: estimate.power_production_at_time( + estimate.now() + timedelta(hours=24) + ) + / 1000, name="Estimated Power Production - Next 24 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -77,11 +93,13 @@ SENSORS: list[ForecastSolarSensor] = [ ForecastSolarSensor( key="energy_current_hour", name="Estimated Energy Production - This Hour", + state=lambda estimate: estimate.energy_current_hour / 1000, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensor( key="energy_next_hour", + state=lambda estimate: estimate.sum_energy_production(1) / 1000, name="Estimated Energy Production - Next Hour", device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json index c17e8bd51f8..2b57eed84ac 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -3,7 +3,7 @@ "name": "Forecast.Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/forecast_solar", - "requirements": ["forecast_solar==1.3.1"], + "requirements": ["forecast_solar==2.0.0"], "codeowners": ["@klaasnicolaas", "@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/forecast_solar/models.py b/homeassistant/components/forecast_solar/models.py index d01f17fc975..a10f52ebcd3 100644 --- a/homeassistant/components/forecast_solar/models.py +++ b/homeassistant/components/forecast_solar/models.py @@ -2,6 +2,9 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any, Callable + +from forecast_solar.models import Estimate @dataclass @@ -13,5 +16,6 @@ class ForecastSolarSensor: device_class: str | None = None entity_registry_enabled_default: bool = True + state: Callable[[Estimate], Any] | None = None state_class: str | None = None unit_of_measurement: str | None = None diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index b32f1f341be..e73b2105b8e 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -66,7 +66,13 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): @property def state(self) -> StateType: """Return the state of the sensor.""" - state: StateType | datetime = getattr(self.coordinator.data, self._sensor.key) + if self._sensor.state is None: + state: StateType | datetime = getattr( + self.coordinator.data, self._sensor.key + ) + else: + state = self._sensor.state(self.coordinator.data) + if isinstance(state, datetime): return state.isoformat() return state diff --git a/requirements_all.txt b/requirements_all.txt index 50ca781cfc8..927a685ded4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -630,7 +630,7 @@ fnvhash==0.1.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast_solar==1.3.1 +forecast_solar==2.0.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88b251137ed..101e0bbe4b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -348,7 +348,7 @@ fnvhash==0.1.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast_solar==1.3.1 +forecast_solar==2.0.0 # homeassistant.components.freebox freebox-api==0.0.10 diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index c2b5fc08181..88f3bf9d4a4 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -1,9 +1,10 @@ """Fixtures for Forecast.Solar integration tests.""" -import datetime +from datetime import datetime, timedelta from typing import Generator from unittest.mock import MagicMock, patch +from forecast_solar import models import pytest from homeassistant.components.forecast_solar.const import ( @@ -16,6 +17,7 @@ from homeassistant.components.forecast_solar.const import ( from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -54,24 +56,31 @@ def mock_forecast_solar() -> Generator[None, MagicMock, None]: "homeassistant.components.forecast_solar.ForecastSolar", autospec=True ) as forecast_solar_mock: forecast_solar = forecast_solar_mock.return_value + now = datetime(2021, 6, 27, 6, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE) - estimate = MagicMock() + estimate = MagicMock(spec_set=models.Estimate) + estimate.now.return_value = now estimate.timezone = "Europe/Amsterdam" - estimate.energy_production_today = 100 - estimate.energy_production_tomorrow = 200 - estimate.power_production_now = 300 - estimate.power_highest_peak_time_today = datetime.datetime( - 2021, 6, 27, 13, 0, tzinfo=datetime.timezone.utc + estimate.energy_production_today = 100000 + estimate.energy_production_tomorrow = 200000 + estimate.power_production_now = 300000 + estimate.power_highest_peak_time_today = datetime( + 2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE ) - estimate.power_highest_peak_time_tomorrow = datetime.datetime( - 2021, 6, 27, 14, 0, tzinfo=datetime.timezone.utc + estimate.power_highest_peak_time_tomorrow = datetime( + 2021, 6, 27, 14, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE ) - estimate.power_production_next_hour = 400 - estimate.power_production_next_6hours = 500 - estimate.power_production_next_12hours = 600 - estimate.power_production_next_24hours = 700 - estimate.energy_current_hour = 800 - estimate.energy_next_hour = 900 + estimate.energy_current_hour = 800000 + + estimate.power_production_at_time.side_effect = { + now + timedelta(hours=1): 400000, + now + timedelta(hours=12): 600000, + now + timedelta(hours=24): 700000, + }.get + + estimate.sum_energy_production.side_effect = { + 1: 900000, + }.get forecast_solar.estimate.return_value = estimate yield forecast_solar diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 31c367678c1..8b8c1cc933e 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -40,7 +40,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_energy_production_today" - assert state.state == "100" + assert state.state == "100.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Energy Production - Today" @@ -55,7 +55,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_energy_production_tomorrow" - assert state.state == "200" + assert state.state == "200.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Energy Production - Tomorrow" @@ -96,7 +96,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_production_now" - assert state.state == "300" + assert state.state == "300.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" ) @@ -110,7 +110,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_energy_current_hour" - assert state.state == "800" + assert state.state == "800.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Energy Production - This Hour" @@ -125,7 +125,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_energy_next_hour" - assert state.state == "900" + assert state.state == "900.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Energy Production - Next Hour" @@ -175,17 +175,17 @@ async def test_disabled_by_default( ( "power_production_next_12hours", "Estimated Power Production - Next 12 Hours", - "600", + "600.0", ), ( "power_production_next_24hours", "Estimated Power Production - Next 24 Hours", - "700", + "700.0", ), ( "power_production_next_hour", "Estimated Power Production - Next Hour", - "400", + "400.0", ), ], ) From ecf0d4398dae241ba9b0c31f76e728c1e4d494a0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 22 Jul 2021 00:10:31 +0000 Subject: [PATCH 487/818] [ci skip] Translation update --- .../components/abode/translations/de.json | 2 +- .../components/adax/translations/ca.json | 20 +++++++++++ .../components/adax/translations/de.json | 20 +++++++++++ .../components/adax/translations/en.json | 9 +++-- .../components/adax/translations/et.json | 20 +++++++++++ .../components/adax/translations/ru.json | 20 +++++++++++ .../components/adguard/translations/de.json | 2 +- .../advantage_air/translations/de.json | 2 +- .../components/airvisual/translations/de.json | 4 +-- .../airvisual/translations/sensor.de.json | 20 +++++++++++ .../ambiclimate/translations/de.json | 2 +- .../ambient_station/translations/de.json | 2 +- .../components/atag/translations/de.json | 2 +- .../binary_sensor/translations/de.json | 14 ++++---- .../components/blebox/translations/de.json | 2 +- .../components/bond/translations/de.json | 2 +- .../components/broadlink/translations/de.json | 4 +-- .../components/brother/translations/de.json | 2 +- .../components/bsblan/translations/de.json | 2 +- .../components/cast/translations/de.json | 2 +- .../cert_expiry/translations/de.json | 2 +- .../cloudflare/translations/de.json | 2 +- .../components/co2signal/translations/de.json | 34 +++++++++++++++++++ .../components/coinbase/translations/de.json | 1 + .../components/control4/translations/de.json | 2 +- .../coronavirus/translations/de.json | 2 +- .../components/daikin/translations/de.json | 2 +- .../components/dexcom/translations/de.json | 2 +- .../components/directv/translations/de.json | 2 +- .../components/doorbird/translations/de.json | 2 +- .../components/dunehd/translations/de.json | 2 +- .../components/ecobee/translations/de.json | 4 +-- .../components/elgato/translations/de.json | 2 +- .../components/enocean/translations/de.json | 2 +- .../components/esphome/translations/de.json | 2 +- .../fireservicerota/translations/de.json | 10 +++--- .../flick_electric/translations/de.json | 2 +- .../components/flipr/translations/ca.json | 30 ++++++++++++++++ .../components/flipr/translations/en.json | 26 +++++++------- .../components/flipr/translations/et.json | 30 ++++++++++++++++ .../forked_daapd/translations/de.json | 2 +- .../components/fritzbox/translations/de.json | 2 +- .../garmin_connect/translations/de.json | 2 +- .../geonetnz_quakes/translations/de.json | 2 +- .../components/gios/translations/de.json | 2 +- .../components/glances/translations/de.json | 4 +-- .../components/goalzero/translations/de.json | 2 +- .../growatt_server/translations/ca.json | 1 + .../growatt_server/translations/de.json | 1 + .../growatt_server/translations/en.json | 1 + .../growatt_server/translations/et.json | 1 + .../growatt_server/translations/ru.json | 1 + .../components/hangouts/translations/de.json | 4 +-- .../hisense_aehw4a1/translations/de.json | 2 +- .../homematicip_cloud/translations/de.json | 2 +- .../components/honeywell/translations/de.json | 17 ++++++++++ .../components/hue/translations/de.json | 2 +- .../components/icloud/translations/de.json | 2 +- .../components/iqvia/translations/de.json | 2 +- .../components/izone/translations/de.json | 2 +- .../components/kodi/translations/de.json | 2 +- .../components/life360/translations/de.json | 2 +- .../components/lifx/translations/de.json | 2 +- .../litterrobot/translations/de.json | 2 +- .../components/melcloud/translations/de.json | 2 +- .../components/metoffice/translations/de.json | 4 +-- .../components/mill/translations/de.json | 2 +- .../components/mqtt/translations/de.json | 2 +- .../components/neato/translations/de.json | 2 +- .../components/netatmo/translations/de.json | 4 +-- .../nfandroidtv/translations/ca.json | 21 ++++++++++++ .../nfandroidtv/translations/de.json | 21 ++++++++++++ .../nfandroidtv/translations/en.json | 2 +- .../nfandroidtv/translations/et.json | 21 ++++++++++++ .../nfandroidtv/translations/ru.json | 21 ++++++++++++ .../nmap_tracker/translations/de.json | 2 +- .../components/nzbget/translations/de.json | 4 +-- .../openweathermap/translations/de.json | 2 +- .../components/pi_hole/translations/de.json | 6 ++-- .../components/plex/translations/de.json | 2 +- .../components/plugwise/translations/de.json | 2 +- .../components/point/translations/de.json | 2 +- .../components/ps4/translations/de.json | 4 +-- .../components/rfxtrx/translations/de.json | 2 +- .../components/roku/translations/de.json | 2 +- .../components/samsungtv/translations/de.json | 2 +- .../components/sense/translations/de.json | 2 +- .../simplisafe/translations/de.json | 2 +- .../smartthings/translations/de.json | 2 +- .../components/smarttub/translations/de.json | 2 +- .../components/solaredge/translations/de.json | 4 +-- .../components/sonarr/translations/de.json | 4 +-- .../components/songpal/translations/de.json | 2 +- .../components/sonos/translations/de.json | 2 +- .../speedtestdotnet/translations/de.json | 2 +- .../srp_energy/translations/de.json | 2 +- .../switcher_kis/translations/de.json | 13 +++++++ .../components/syncthru/translations/de.json | 2 +- .../synology_dsm/translations/ca.json | 11 +++++- .../synology_dsm/translations/de.json | 15 ++------ .../synology_dsm/translations/et.json | 11 +++++- .../synology_dsm/translations/ru.json | 11 +++++- .../tellduslive/translations/de.json | 2 +- .../components/tesla/translations/de.json | 2 +- .../components/tibber/translations/de.json | 2 +- .../components/tile/translations/de.json | 4 +-- .../components/timer/translations/de.json | 2 +- .../components/tplink/translations/de.json | 2 +- .../components/tradfri/translations/de.json | 2 +- .../components/unifi/translations/de.json | 2 +- .../components/upnp/translations/de.json | 4 +-- .../components/vesync/translations/de.json | 2 +- .../components/vilfo/translations/de.json | 6 ++-- .../components/wemo/translations/de.json | 2 +- .../components/wiffi/translations/de.json | 2 +- .../components/wled/translations/de.json | 2 +- .../components/xbox/translations/de.json | 2 +- .../xiaomi_aqara/translations/de.json | 2 +- .../xiaomi_miio/translations/de.json | 2 +- .../zoneminder/translations/de.json | 2 +- .../components/zwave/translations/de.json | 6 ++-- .../components/zwave_js/translations/de.json | 8 +++++ 122 files changed, 496 insertions(+), 157 deletions(-) create mode 100644 homeassistant/components/adax/translations/ca.json create mode 100644 homeassistant/components/adax/translations/de.json create mode 100644 homeassistant/components/adax/translations/et.json create mode 100644 homeassistant/components/adax/translations/ru.json create mode 100644 homeassistant/components/airvisual/translations/sensor.de.json create mode 100644 homeassistant/components/co2signal/translations/de.json create mode 100644 homeassistant/components/flipr/translations/ca.json create mode 100644 homeassistant/components/flipr/translations/et.json create mode 100644 homeassistant/components/honeywell/translations/de.json create mode 100644 homeassistant/components/nfandroidtv/translations/ca.json create mode 100644 homeassistant/components/nfandroidtv/translations/de.json create mode 100644 homeassistant/components/nfandroidtv/translations/et.json create mode 100644 homeassistant/components/nfandroidtv/translations/ru.json create mode 100644 homeassistant/components/switcher_kis/translations/de.json diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json index 307f5f45065..695ecba621c 100644 --- a/homeassistant/components/abode/translations/de.json +++ b/homeassistant/components/abode/translations/de.json @@ -26,7 +26,7 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail-Adresse" + "username": "E-Mail" }, "title": "Gib deine Abode-Anmeldeinformationen ein" } diff --git a/homeassistant/components/adax/translations/ca.json b/homeassistant/components/adax/translations/ca.json new file mode 100644 index 00000000000..85ba15804ac --- /dev/null +++ b/homeassistant/components/adax/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "account_id": "ID del compte", + "host": "Amfitri\u00f3", + "password": "Contrasenya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/de.json b/homeassistant/components/adax/translations/de.json new file mode 100644 index 00000000000..414b373ff34 --- /dev/null +++ b/homeassistant/components/adax/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "account_id": "Konto-ID", + "host": "Host", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/en.json b/homeassistant/components/adax/translations/en.json index a5a204c93f8..d1ef64a52c0 100644 --- a/homeassistant/components/adax/translations/en.json +++ b/homeassistant/components/adax/translations/en.json @@ -5,17 +5,16 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { "user": { "data": { + "account_id": "Account ID", "host": "Host", - "password": "Password", - "account_id": "Account ID" + "password": "Password" } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/et.json b/homeassistant/components/adax/translations/et.json new file mode 100644 index 00000000000..c8dd855218c --- /dev/null +++ b/homeassistant/components/adax/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga" + }, + "step": { + "user": { + "data": { + "account_id": "Konto ID", + "host": "Host", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/ru.json b/homeassistant/components/adax/translations/ru.json new file mode 100644 index 00000000000..d0aece78982 --- /dev/null +++ b/homeassistant/components/adax/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "account_id": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index f73c25d769e..b0a6b480249 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -17,7 +17,7 @@ "host": "Host", "password": "Passwort", "port": "Port", - "ssl": "AdGuard Home verwendet ein SSL-Zertifikat", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index d3eb0296847..0d3ead73fc0 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "ip_address": "IP Adresse", + "ip_address": "IP-Adresse", "port": "Port" }, "description": "Anschluss an die API deines Advantage Air Wandtabletts.", diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index d5b9fd915d3..c6d00ea1375 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.", + "already_configured": "Diese Node/Pro ID oder Standort ist bereits konfiguriert.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { @@ -40,7 +40,7 @@ }, "reauth_confirm": { "data": { - "api_key": "API-Key" + "api_key": "API-Schl\u00fcssel" }, "title": "AirVisual erneut authentifizieren" }, diff --git a/homeassistant/components/airvisual/translations/sensor.de.json b/homeassistant/components/airvisual/translations/sensor.de.json new file mode 100644 index 00000000000..d6aeab515bd --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.de.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Kohlenmonoxid", + "n2": "Stickstoffdioxid", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Schwefeldioxid" + }, + "airvisual__pollutant_level": { + "good": "Gut", + "hazardous": "Gef\u00e4hrlich", + "moderate": "M\u00e4\u00dfig", + "unhealthy": "Ungesund", + "unhealthy_sensitive": "Ungesund f\u00fcr sensible Gruppen", + "very_unhealthy": "Sehr ungesund" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/de.json b/homeassistant/components/ambiclimate/translations/de.json index d91fc15f37d..3f4537a5d5c 100644 --- a/homeassistant/components/ambiclimate/translations/de.json +++ b/homeassistant/components/ambiclimate/translations/de.json @@ -6,7 +6,7 @@ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." }, "create_entry": { - "default": "Erfolgreiche Authentifizierung mit Ambiclimate" + "default": "Erfolgreich authentifiziert" }, "error": { "follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst", diff --git a/homeassistant/components/ambient_station/translations/de.json b/homeassistant/components/ambient_station/translations/de.json index c6570fee0e3..8dda644cc26 100644 --- a/homeassistant/components/ambient_station/translations/de.json +++ b/homeassistant/components/ambient_station/translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API Schl\u00fcssel", + "api_key": "API-Schl\u00fcssel", "app_key": "Anwendungsschl\u00fcssel" }, "title": "Gib deine Informationen ein" diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 8d91f5b62fa..976faaa370d 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index a78befb7965..a2ef817bedb 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -139,8 +139,8 @@ "on": "Nass" }, "motion": { - "off": "Ruhig", - "on": "Bewegung erkannt" + "off": "Normal", + "on": "Erkannt" }, "moving": { "off": "Bewegt sich nicht", @@ -171,16 +171,16 @@ "on": "Unsicher" }, "smoke": { - "off": "OK", - "on": "Rauch erkannt" + "off": "Normal", + "on": "Erkannt" }, "sound": { - "off": "Stille", - "on": "Ger\u00e4usch erkannt" + "off": "Normal", + "on": "Erkannt" }, "vibration": { "off": "Normal", - "on": "Vibration" + "on": "Erkannt" }, "window": { "off": "Geschlossen", diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index bb1f9e9c443..c104a96fe46 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "IP Adresse", + "host": "IP-Adresse", "port": "Port" }, "description": "Richte deine BleBox f\u00fcr die Integration mit dem Home Assistant ein.", diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 934e166e0d5..51f0bd0bee4 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "access_token": "Zugriffstoken", + "access_token": "Zugangstoken", "host": "Host" } } diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index e0e819e2140..1e5635b3145 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -4,13 +4,13 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "not_supported": "Ger\u00e4t nicht unterst\u00fctzt", "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name} ({model} unter {host})", diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 7b9f811ac32..b79ff0a7619 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieser Drucker ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." }, "error": { diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index ce9d8a0cb00..079749f0f7a 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -13,7 +13,7 @@ "host": "Host", "passkey": "Passkey String", "password": "Passwort", - "port": "Port Nummer", + "port": "Port", "username": "Benutzername" }, "description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.", diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 3e03e6a3b73..b337a8575e0 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein." diff --git a/homeassistant/components/cert_expiry/translations/de.json b/homeassistant/components/cert_expiry/translations/de.json index 2c01c9f71a6..640e715b359 100644 --- a/homeassistant/components/cert_expiry/translations/de.json +++ b/homeassistant/components/cert_expiry/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Diese Kombination aus Host und Port ist bereits konfiguriert.", + "already_configured": "Der Dienst ist bereits konfiguriert", "import_failed": "Import aus Konfiguration fehlgeschlagen" }, "error": { diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index d03f293b38b..98cdbe355f6 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -26,7 +26,7 @@ }, "user": { "data": { - "api_token": "API Token" + "api_token": "API-Token" }, "description": "F\u00fcr diese Integration ist ein API-Token erforderlich, der mit Zone: Zone: Lesen und Zone: DNS: Bearbeiten f\u00fcr alle Zonen in deinem Konto erstellt wurde.", "title": "Mit Cloudflare verbinden" diff --git a/homeassistant/components/co2signal/translations/de.json b/homeassistant/components/co2signal/translations/de.json new file mode 100644 index 00000000000..e35b991566f --- /dev/null +++ b/homeassistant/components/co2signal/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "api_ratelimit": "API Ratelimit \u00fcberschritten", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "api_ratelimit": "API Ratelimit \u00fcberschritten", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + } + }, + "country": { + "data": { + "country_code": "L\u00e4ndercode" + } + }, + "user": { + "data": { + "api_key": "Zugangstoken", + "location": "Daten abrufen f\u00fcr" + }, + "description": "Besuche https://co2signal.com/, um ein Token anzufordern." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index e1fb1fdbcad..25d20fe8cf2 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Zu meldende Wallet-Guthaben.", + "exchange_base": "Basisw\u00e4hrung f\u00fcr Wechselkurssensoren.", "exchange_rate_currencies": "Zu meldende Wechselkurse." }, "description": "Coinbase-Optionen anpassen" diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json index e50e2499320..4c9ef9abf11 100644 --- a/homeassistant/components/control4/translations/de.json +++ b/homeassistant/components/control4/translations/de.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "IP-Addresse", + "host": "IP-Adresse", "password": "Passwort", "username": "Benutzername" }, diff --git a/homeassistant/components/coronavirus/translations/de.json b/homeassistant/components/coronavirus/translations/de.json index 25a1cf44ca5..24da7b952ea 100644 --- a/homeassistant/components/coronavirus/translations/de.json +++ b/homeassistant/components/coronavirus/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses Land ist bereits konfiguriert.", + "already_configured": "Der Dienst ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index dcec53c1569..038a997201a 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -16,7 +16,7 @@ "host": "Host", "password": "Passwort" }, - "description": "Gib die IP-Adresse deiner Daikin AC ein.", + "description": "Gib die IP-Adresse deiner Daikin AC ein.\n\nBeachte, dass API-Schl\u00fcssel und Passwort nur von BRP072Cxx bzw. SKYFi-Ger\u00e4ten verwendet werden.", "title": "Daikin AC konfigurieren" } } diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index 20e5ee22751..be04c779390 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/directv/translations/de.json b/homeassistant/components/directv/translations/de.json index c4a6ed1791e..5f06a68e715 100644 --- a/homeassistant/components/directv/translations/de.json +++ b/homeassistant/components/directv/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Das Ger\u00e4t ist bereits konfiguriert.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "unknown": "Unerwarteter Fehler" }, "error": { diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index b558bd0b222..3f025e67386 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifikation", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/dunehd/translations/de.json b/homeassistant/components/dunehd/translations/de.json index aa87de530b8..f3d7ecd725a 100644 --- a/homeassistant/components/dunehd/translations/de.json +++ b/homeassistant/components/dunehd/translations/de.json @@ -6,7 +6,7 @@ "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse." + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse" }, "step": { "user": { diff --git a/homeassistant/components/ecobee/translations/de.json b/homeassistant/components/ecobee/translations/de.json index 0c89a696b2c..10edbd4ecd1 100644 --- a/homeassistant/components/ecobee/translations/de.json +++ b/homeassistant/components/ecobee/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Bereits eingerichtet. Es ist nur eine Konfiguration m\u00f6glich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.", @@ -14,7 +14,7 @@ }, "user": { "data": { - "api_key": "API Schl\u00fcssel" + "api_key": "API-Schl\u00fcssel" }, "description": "Bitte gib den von ecobee.com erhaltenen API-Schl\u00fcssel ein.", "title": "ecobee API-Schl\u00fcssel" diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index 95bb2609d84..6ff531919cb 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json index fe7467fbf09..63a3cf73ca8 100644 --- a/homeassistant/components/enocean/translations/de.json +++ b/homeassistant/components/enocean/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "Ung\u00fcltiger Dongle-Pfad", - "single_instance_allowed": "Schon konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "invalid_dongle_path": "Kein g\u00fcltiger Dongle unter diesem Pfad gefunden" diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index c82afc78851..8084ef26f0e 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "ESP ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { diff --git a/homeassistant/components/fireservicerota/translations/de.json b/homeassistant/components/fireservicerota/translations/de.json index 737fbc5ff53..c8c18c4c372 100644 --- a/homeassistant/components/fireservicerota/translations/de.json +++ b/homeassistant/components/fireservicerota/translations/de.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "Account wurde schon konfiguriert", - "reauth_successful": "Neuauthentifizierung erfolgreich" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "create_entry": { - "default": "Authentifizierung erfolgreich" + "default": "Erfolgreich authentifiziert" }, "error": { - "invalid_auth": "Authentifizienung ung\u00fcltig" + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "reauth": { @@ -21,7 +21,7 @@ "data": { "password": "Passwort", "url": "Webseite", - "username": "Nutzername" + "username": "Benutzername" } } } diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index 13ae8555608..8409250c5fa 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/flipr/translations/ca.json b/homeassistant/components/flipr/translations/ca.json new file mode 100644 index 00000000000..fcb43623030 --- /dev/null +++ b/homeassistant/components/flipr/translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_flipr_id_found": "De moment, no hi ha cap identificador de Flipr associat al teu compte. Primer hauries de verificar que funciona amb l'aplicaci\u00f3 m\u00f2bil de Flipr.", + "unknown": "Error inesperat" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "ID Flipr" + }, + "description": "Tria l'ID Flipr de la llista", + "title": "Tria el teu Flipr" + }, + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "description": "Connecta't amb el teu compte de Flipr.", + "title": "Connexi\u00f3 amb Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/en.json b/homeassistant/components/flipr/translations/en.json index 017514e147c..667824d407b 100644 --- a/homeassistant/components/flipr/translations/en.json +++ b/homeassistant/components/flipr/translations/en.json @@ -1,30 +1,30 @@ { "config": { "abort": { - "already_configured": "This Flipr is already configured" + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", - "no_flipr_id_found": "No flipr id associated to your account for now. You should verify it is working with the Flipr's mobile app first." + "no_flipr_id_found": "No flipr id associated to your account for now. You should verify it is working with the Flipr's mobile app first.", + "unknown": "Unexpected error" }, "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Choose your Flipr ID in the list", + "title": "Choose your Flipr" + }, "user": { "data": { "email": "Email", "password": "Password" }, - "description": "Connect to your flipr account", - "title": "Flipr device" - }, - "flipr_id": { - "data": { - "flipr_id": "Flipr ID" - }, - "description": "Choose your flipr ID in the list", - "title": "Flipr device" + "description": "Connect using your Flipr account.", + "title": "Connect to Flipr" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/et.json b/homeassistant/components/flipr/translations/et.json new file mode 100644 index 00000000000..46be2f4378f --- /dev/null +++ b/homeassistant/components/flipr/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "no_flipr_id_found": "Kontoga pole praegu \u00fchtegi flipr-it seostatud. K\u00f5igepealt pead kontrollima, kas see t\u00f6\u00f6tab Flipri mobiilirakendusega.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipri ID" + }, + "description": "Vali loendist oma Flipri ID", + "title": "Vali oma Flipr" + }, + "user": { + "data": { + "email": "E-post", + "password": "Salas\u00f5na" + }, + "description": "\u00dchenda oma Flipr konto abil.", + "title": "Flipriga \u00fchenduse loomine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index a475becc605..51fd312fd6d 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -6,7 +6,7 @@ }, "error": { "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfe deine forked-daapd-Netzwerkberechtigungen.", - "unknown_error": "Unbekannter Fehler", + "unknown_error": "Unerwarteter Fehler", "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index ceaca6fd19a..7da8e616cfc 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -8,7 +8,7 @@ "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "invalid_auth": "Ung\u00fcltige Zugangsdaten" + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "AVM FRITZ!Box: {name}", "step": { diff --git a/homeassistant/components/garmin_connect/translations/de.json b/homeassistant/components/garmin_connect/translations/de.json index 7817a44f6c0..d6310595ad8 100644 --- a/homeassistant/components/garmin_connect/translations/de.json +++ b/homeassistant/components/garmin_connect/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses Konto ist bereits konfiguriert." + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/geonetnz_quakes/translations/de.json b/homeassistant/components/geonetnz_quakes/translations/de.json index 583712c6c4e..2bfc3f2dbbd 100644 --- a/homeassistant/components/geonetnz_quakes/translations/de.json +++ b/homeassistant/components/geonetnz_quakes/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Standort ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/gios/translations/de.json b/homeassistant/components/gios/translations/de.json index e1351278f38..99548187601 100644 --- a/homeassistant/components/gios/translations/de.json +++ b/homeassistant/components/gios/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "GIO\u015a integration f\u00fcr diese Messstation ist bereits konfiguriert. " + "already_configured": "Standort ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/glances/translations/de.json b/homeassistant/components/glances/translations/de.json index e464bfdee34..8c91e4fb2e3 100644 --- a/homeassistant/components/glances/translations/de.json +++ b/homeassistant/components/glances/translations/de.json @@ -14,9 +14,9 @@ "name": "Name", "password": "Passwort", "port": "Port", - "ssl": "Verwende SSL / TLS, um eine Verbindung zum Glances-System herzustellen", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", - "verify_ssl": "\u00dcberpr\u00fcfe die Zertifizierung des Systems", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen", "version": "Glances API-Version (2 oder 3)" }, "title": "Glances einrichten" diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index d483564fa72..5133488c247 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/growatt_server/translations/ca.json b/homeassistant/components/growatt_server/translations/ca.json index 0c1e1b6cb83..39dc1153434 100644 --- a/homeassistant/components/growatt_server/translations/ca.json +++ b/homeassistant/components/growatt_server/translations/ca.json @@ -17,6 +17,7 @@ "data": { "name": "Nom", "password": "Contrasenya", + "url": "URL", "username": "Nom d'usuari" }, "title": "Introdueix la teva informaci\u00f3 de Growatt" diff --git a/homeassistant/components/growatt_server/translations/de.json b/homeassistant/components/growatt_server/translations/de.json index ae24396823a..adb769baa2d 100644 --- a/homeassistant/components/growatt_server/translations/de.json +++ b/homeassistant/components/growatt_server/translations/de.json @@ -17,6 +17,7 @@ "data": { "name": "Name", "password": "Passwort", + "url": "URL", "username": "Benutzername" }, "title": "Gib deine Growatt-Informationen ein" diff --git a/homeassistant/components/growatt_server/translations/en.json b/homeassistant/components/growatt_server/translations/en.json index 5461c822320..86196783133 100644 --- a/homeassistant/components/growatt_server/translations/en.json +++ b/homeassistant/components/growatt_server/translations/en.json @@ -17,6 +17,7 @@ "data": { "name": "Name", "password": "Password", + "url": "URL", "username": "Username" }, "title": "Enter your Growatt information" diff --git a/homeassistant/components/growatt_server/translations/et.json b/homeassistant/components/growatt_server/translations/et.json index 3115713bc68..c3327e3d676 100644 --- a/homeassistant/components/growatt_server/translations/et.json +++ b/homeassistant/components/growatt_server/translations/et.json @@ -17,6 +17,7 @@ "data": { "name": "Nimi", "password": "Salas\u00f5na", + "url": "URL", "username": "Kasutajanimi" }, "title": "Sisesta oma Growatti teave" diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json index 7d866ba09b0..0b98838dac8 100644 --- a/homeassistant/components/growatt_server/translations/ru.json +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -17,6 +17,7 @@ "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Growatt." diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index 7b888cf531e..42770308346 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts ist bereits konfiguriert", + "already_configured": "Der Dienst ist bereits konfiguriert", "unknown": "Unerwarteter Fehler" }, "error": { @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Autorisierungscode (f\u00fcr die manuelle Authentifizierung erforderlich)", - "email": "E-Mail-Adresse", + "email": "E-Mail", "password": "Passwort" }, "description": "Leer", diff --git a/homeassistant/components/hisense_aehw4a1/translations/de.json b/homeassistant/components/hisense_aehw4a1/translations/de.json index 7c0bd96a9c9..03e15051eb0 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/de.json +++ b/homeassistant/components/hisense_aehw4a1/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Es wurden keine Hisense AEH-W4A1-Ger\u00e4te im Netzwerk gefunden.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/homematicip_cloud/translations/de.json b/homeassistant/components/homematicip_cloud/translations/de.json index 1da1e06c0fb..3cb74491c7f 100644 --- a/homeassistant/components/homematicip_cloud/translations/de.json +++ b/homeassistant/components/homematicip_cloud/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Accesspoint ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "connection_aborted": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/honeywell/translations/de.json b/homeassistant/components/honeywell/translations/de.json new file mode 100644 index 00000000000..a146d442eef --- /dev/null +++ b/homeassistant/components/honeywell/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte gib die Anmeldedaten ein, mit denen du dich bei mytotalconnectcomfort.com anmeldest.", + "title": "Honeywell Total Connect Comfort (US)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index 122e1ba6f5c..bf0d2a7c756 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -8,7 +8,7 @@ "discover_timeout": "Es k\u00f6nnen keine Hue Bridges erkannt werden", "no_bridges": "Keine Philips Hue Bridges erkannt", "not_hue_bridge": "Keine Philips Hue Bridge entdeckt", - "unknown": "Unbekannter Fehler ist aufgetreten" + "unknown": "Unerwarteter Fehler" }, "error": { "linking": "Unerwarteter Fehler", diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 4cc7ed93eef..207735018f0 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -28,7 +28,7 @@ "user": { "data": { "password": "Passwort", - "username": "Email", + "username": "E-Mail", "with_family": "Mit Familie" }, "description": "Gib deine Zugangsdaten ein", diff --git a/homeassistant/components/iqvia/translations/de.json b/homeassistant/components/iqvia/translations/de.json index 1318a9c90cc..5d307eea829 100644 --- a/homeassistant/components/iqvia/translations/de.json +++ b/homeassistant/components/iqvia/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { "invalid_zip_code": "Postleitzahl ist ung\u00fcltig" diff --git a/homeassistant/components/izone/translations/de.json b/homeassistant/components/izone/translations/de.json index f6e03c3af27..6b9441d1683 100644 --- a/homeassistant/components/izone/translations/de.json +++ b/homeassistant/components/izone/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 9e486ed119b..478fa2c32e5 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -29,7 +29,7 @@ "data": { "host": "Host", "port": "Port", - "ssl": "Verwendet ein SSL Zertifikat" + "ssl": "Verwendet ein SSL-Zertifikat" }, "description": "Kodi-Verbindungsinformationen. Bitte stelle sicher, dass du \"Steuerung von Kodi \u00fcber HTTP zulassen\" in System/Einstellungen/Netzwerk/Dienste aktiviert hast." }, diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 7e495987b45..516b0255349 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -8,7 +8,7 @@ "default": "M\u00f6gliche erweiterte Einstellungen finden sich unter [Life360-Dokumentation]({docs_url})." }, "error": { - "already_configured": "Konto ist bereits konfiguriert", + "already_configured": "Konto wurde bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index 83eded1ddc6..0c619ea4062 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json index c8f4f35716e..14f319fb4d3 100644 --- a/homeassistant/components/litterrobot/translations/de.json +++ b/homeassistant/components/litterrobot/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/melcloud/translations/de.json b/homeassistant/components/melcloud/translations/de.json index a0d6ce662ba..e983af740e2 100644 --- a/homeassistant/components/melcloud/translations/de.json +++ b/homeassistant/components/melcloud/translations/de.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail-Adresse" + "username": "E-Mail" }, "description": "Verbinde dich mit deinem MELCloud-Konto.", "title": "Stelle eine Verbindung zu MELCloud her" diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 8f35c2aaeaa..2c28d0742ce 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API Key", + "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, diff --git a/homeassistant/components/mill/translations/de.json b/homeassistant/components/mill/translations/de.json index 63b6b7ea6e9..44d9c2448e6 100644 --- a/homeassistant/components/mill/translations/de.json +++ b/homeassistant/components/mill/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Account ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 132b4c42e18..2961a69ed1b 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 4b0722a207c..c7fd239c585 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index e1a2c3c93cc..becff2df430 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -7,11 +7,11 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "Erfolgreich authentifiziert." + "default": "Erfolgreich authentifiziert" }, "step": { "pick_implementation": { - "title": "W\u00e4hle Authentifizierungs-Methode" + "title": "W\u00e4hle die Authentifizierungsmethode" } } }, diff --git a/homeassistant/components/nfandroidtv/translations/ca.json b/homeassistant/components/nfandroidtv/translations/ca.json new file mode 100644 index 00000000000..861ad41a39b --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom" + }, + "description": "Aquesta integraci\u00f3 necessita l'aplicaci\u00f3 Notificacions per a Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nHauries de configurar o b\u00e9 una reserva DHCP al router (consulta el manual del teu rounter) o b\u00e9 adre\u00e7a IP est\u00e0tica al dispositiu. Si no o fas, el disositiu acabar\u00e0 deixant d'estar disponible.", + "title": "Notificacions per a Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/de.json b/homeassistant/components/nfandroidtv/translations/de.json new file mode 100644 index 00000000000..b3adce9ac07 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + }, + "description": "Diese Integration erfordert die App \"Benachrichtigungen f\u00fcr Android TV\".\n\nF\u00fcr Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nF\u00fcr Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDu solltest entweder eine DHCP-Reservierung auf deinem Router (siehe Benutzerhandbuch deines Routers) oder eine statische IP-Adresse auf dem Ger\u00e4t einrichten. Andernfalls wird das Ger\u00e4t irgendwann nicht mehr verf\u00fcgbar sein.", + "title": "Benachrichtigungen f\u00fcr Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/en.json b/homeassistant/components/nfandroidtv/translations/en.json index 22d014c1ffa..f117428df35 100644 --- a/homeassistant/components/nfandroidtv/translations/en.json +++ b/homeassistant/components/nfandroidtv/translations/en.json @@ -18,4 +18,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/et.json b/homeassistant/components/nfandroidtv/translations/et.json new file mode 100644 index 00000000000..f2405ab1421 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nimi" + }, + "description": "See sidumine n\u00f5uab Android TV rakenduse Notifications for Android TV kasutamist.\n\nAndroid TV jaoks: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV jaoks: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nPead seadma ruuterile kas DHCP-reservatsiooni (vt ruuteri kasutusjuhendit) v\u00f5i seadme staatilise IP-aadressi. Vastasel juhul muutub seade l\u00f5puks k\u00e4ttesaamatuks.", + "title": "Android TV / Fire TV teavitused" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/ru.json b/homeassistant/components/nfandroidtv/translations/ru.json new file mode 100644 index 00000000000..ce0d4651dfc --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0414\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \"Notifications for Android TV\". \n\n\u0414\u043b\u044f Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n\u0414\u043b\u044f Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 DHCP \u043d\u0430 \u0432\u0430\u0448\u0435\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435 (\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0443) \u0438\u043b\u0438 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c.", + "title": "Notifications for Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json index 1893eb28b08..729a964059f 100644 --- a/homeassistant/components/nmap_tracker/translations/de.json +++ b/homeassistant/components/nmap_tracker/translations/de.json @@ -27,7 +27,7 @@ "data": { "exclude": "Netzwerkadressen (kommagetrennt), die von der \u00dcberpr\u00fcfung ausgeschlossen werden sollen", "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", - "hosts": "Zu scannende Netzwerkadressen (kommagetrennt)", + "hosts": "Netzwerkadressen (kommagetrennt) zum Scannen", "interval_seconds": "Scanintervall", "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap", "track_new_devices": "Neue Ger\u00e4te verfolgen" diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index 74d073ce292..1b1575769bc 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -15,9 +15,9 @@ "name": "Name", "password": "Passwort", "port": "Port", - "ssl": "Nutzt ein SSL-Zertifikat", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", - "verify_ssl": "SSL-Zertifikat verfizieren" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "title": "Mit NZBGet verbinden" } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 7b2806693f0..615a642a859 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API Key", + "api_key": "API-Schl\u00fcssel", "language": "Sprache", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 6d9518490d5..40a5db3c21f 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" @@ -16,10 +16,10 @@ "data": { "api_key": "API-Schl\u00fcssel", "host": "Host", - "location": "Org", + "location": "Standort", "name": "Name", "port": "Port", - "ssl": "Nutzt ein SSL-Zertifikat", + "ssl": "Verwendet ein SSL-Zertifikat", "statistics_only": "Nur Statistiken", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index ba2a2c52229..130d34505d2 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -21,7 +21,7 @@ "data": { "host": "Host", "port": "Port", - "ssl": "SSL verwenden", + "ssl": "Verwendet ein SSL-Zertifikat", "token": "Token (optional)", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 4e4bc8baeee..a5c11645d6f 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index c2764e08da9..f0c2eee923b 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -12,7 +12,7 @@ }, "error": { "follow_link": "Bitte folgen dem Link und authentifiziere dich, bevor du auf Senden klickst", - "no_token": "Ung\u00fcltiger Access Token" + "no_token": "Ung\u00fcltiger Zugriffs-Token" }, "step": { "auth": { diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 59a84e7ad27..ca1a0ab24b1 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "credential_error": "Fehler beim Abrufen der Anmeldeinformationen.", - "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "port_987_bind_error": "Konnte sich nicht an Port 987 binden. Weitere Informationen findest du in der [Dokumentation] (https://www.home-assistant.io/components/ps4/).", "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", + "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr den PIN-Code auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN-Code ein. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index a806afb6dbf..7b006782d96 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich.", + "already_configured": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 5f72b4bdd9b..ce8ec9e4595 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index 5e79709f5bd..f59004a5dab 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieser Samsung TV ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe den Ger\u00e4teverbindungsmanager in den Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json index 27cfcc5e5dc..df36684c8b4 100644 --- a/homeassistant/components/sense/translations/de.json +++ b/homeassistant/components/sense/translations/de.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "E-Mail-Adresse", + "email": "E-Mail", "password": "Passwort" }, "title": "Stelle eine Verbindung zu deinem Sense Energy Monitor her" diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index e4966581fac..966f3598d95 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -26,7 +26,7 @@ "data": { "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", "password": "Passwort", - "username": "E-Mail-Adresse" + "username": "E-Mail" }, "title": "Gib deine Informationen ein" } diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index cd946ca8261..6cd7157b702 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -17,7 +17,7 @@ }, "pat": { "data": { - "access_token": "Zugriffs-Token" + "access_token": "Zugangstoken" }, "description": "Bitte gib ein SmartThings [Personal Access Token] ({token_url}) ein, das gem\u00e4\u00df den [Anweisungen] ({component_url}) erstellt wurde. Dies wird zur Erstellung der Home Assistant-Integration in deinem SmartThings-Konto verwendet.", "title": "Gib den pers\u00f6nlichen Zugangstoken an" diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json index 4549360f761..a529f679868 100644 --- a/homeassistant/components/smarttub/translations/de.json +++ b/homeassistant/components/smarttub/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert", + "already_configured": "Konto wurde bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/solaredge/translations/de.json b/homeassistant/components/solaredge/translations/de.json index 20fc557e5c8..247187f35af 100644 --- a/homeassistant/components/solaredge/translations/de.json +++ b/homeassistant/components/solaredge/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Das Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "could_not_connect": "Es konnte keine Verbindung zur Solaredge-API hergestellt werden", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", "site_not_active": "Die Seite ist nicht aktiv" diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index c7ca7bd692b..9779a985034 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Der Dienst ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "unknown": "Unerwateter Fehler" + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -17,7 +17,7 @@ }, "user": { "data": { - "api_key": "API Schl\u00fcssel", + "api_key": "API-Schl\u00fcssel", "base_path": "Pfad zur API", "host": "Host", "port": "Port", diff --git a/homeassistant/components/songpal/translations/de.json b/homeassistant/components/songpal/translations/de.json index ae1695eaa2d..97ba487f525 100644 --- a/homeassistant/components/songpal/translations/de.json +++ b/homeassistant/components/songpal/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "not_songpal_device": "Kein Songpal-Ger\u00e4t" }, "error": { diff --git a/homeassistant/components/sonos/translations/de.json b/homeassistant/components/sonos/translations/de.json index 3860d56387d..0c799072010 100644 --- a/homeassistant/components/sonos/translations/de.json +++ b/homeassistant/components/sonos/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "not_sonos_device": "Erkanntes Ger\u00e4t ist kein Sonos-Ger\u00e4t", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 79a47cbcd2e..eff51fab0b4 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "wrong_server_id": "Server-ID ist ung\u00fcltig" }, "step": { diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json index 45f8ed451a4..a7992cac9b1 100644 --- a/homeassistant/components/srp_energy/translations/de.json +++ b/homeassistant/components/srp_energy/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_account": "Die Konto-ID sollte eine 9-stellige Nummer sein", - "invalid_auth": "Ung\u00fcltige Anmeldung", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/switcher_kis/translations/de.json b/homeassistant/components/switcher_kis/translations/de.json new file mode 100644 index 00000000000..19cd4b8c70e --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json index f7533630216..699b88286dc 100644 --- a/homeassistant/components/syncthru/translations/de.json +++ b/homeassistant/components/syncthru/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist schon konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "invalid_url": "Ung\u00fcltige URL", diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index e08ed1d74ce..2ac5d16b286 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -29,6 +30,14 @@ "description": "Vols configurar {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Motiu: {details}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3 Synology DSM" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 5a6c52872db..5d769dbe7d6 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -25,19 +24,11 @@ "port": "Port", "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", - "verify_ssl": "SSL Zertifikat verifizieren" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "M\u00f6chtest du {name} ({host}) einrichten?", "title": "Synology DSM" }, - "reauth": { - "data": { - "password": "Passwort", - "username": "Benutzername" - }, - "description": "Ursache: {details}", - "title": "Synology DSM erneute Authentifizierung notwendig" - }, "user": { "data": { "host": "Host", @@ -45,7 +36,7 @@ "port": "Port", "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", - "verify_ssl": "SSL Zertifikat verifizieren" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "title": "Synology DSM" } diff --git a/homeassistant/components/synology_dsm/translations/et.json b/homeassistant/components/synology_dsm/translations/et.json index 7d192828d67..eebfd25938b 100644 --- a/homeassistant/components/synology_dsm/translations/et.json +++ b/homeassistant/components/synology_dsm/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -29,6 +30,14 @@ "description": "Kas soovid seadistada {name}({host})?", "title": "" }, + "reauth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "P\u00f5hjus: {details}", + "title": "Synology DSM: Taastuvasta sidumine" + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 9a7157b6dc3..4a2963dc5d5 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -29,6 +30,14 @@ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041f\u0440\u0438\u0447\u0438\u043d\u0430: {details}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f Synology DSM" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index 0a952ba013b..adb5f0e2542 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dienst ist bereits konfiguriert", + "already_configured": "Der Dienst ist bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "unknown": "Unerwarteter Fehler", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index 2fd964fe013..bdcd8237b3b 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -13,7 +13,7 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail-Adresse" + "username": "E-Mail" }, "description": "Bitte gib deine Daten ein.", "title": "Tesla - Konfiguration" diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index d6339bbf20b..f3f722ae835 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "access_token": "Zugriffs-Token" + "access_token": "Zugangstoken" }, "description": "Gib dein Zugangsk\u00fcrzel von https://developer.tibber.com/settings/accesstoken ein.", "title": "Tibber" diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json index 1c2af82aa63..5866a1e0f5b 100644 --- a/homeassistant/components/tile/translations/de.json +++ b/homeassistant/components/tile/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" @@ -10,7 +10,7 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail Adresse" + "username": "E-Mail" }, "title": "Kachel konfigurieren" } diff --git a/homeassistant/components/timer/translations/de.json b/homeassistant/components/timer/translations/de.json index 47cf5b15f23..ba24845aadb 100644 --- a/homeassistant/components/timer/translations/de.json +++ b/homeassistant/components/timer/translations/de.json @@ -2,7 +2,7 @@ "state": { "_": { "active": "Aktiv", - "idle": "Leerlauf", + "idle": "Unt\u00e4tig", "paused": "Pausiert" } } diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json index 48571158085..6f804a6eeef 100644 --- a/homeassistant/components/tplink/translations/de.json +++ b/homeassistant/components/tplink/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Es wurden keine TP-Link-Ger\u00e4te im Netzwerk gefunden.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/tradfri/translations/de.json b/homeassistant/components/tradfri/translations/de.json index b1ebb2aff0b..ee72be60028 100644 --- a/homeassistant/components/tradfri/translations/de.json +++ b/homeassistant/components/tradfri/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Bridge ist bereits konfiguriert.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index bed0cc289ab..ab7f9bb9b16 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -57,7 +57,7 @@ "simple_options": { "data": { "block_client": "Clients mit Netzwerkzugriffskontrolle", - "track_clients": "Netzwerger\u00e4te \u00fcberwachen", + "track_clients": "Nachverfolgen von Netzwerkclients", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)" }, "description": "Konfiguriere die UniFi-Integration" diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index 51be5a6b506..b63d17947ae 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "incomplete_discovery": "Unvollst\u00e4ndige Suche", - "no_devices_found": "Keine UPnP/IGD-Ger\u00e4te im Netzwerk gefunden." + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" }, "error": { "one": "Ein", diff --git a/homeassistant/components/vesync/translations/de.json b/homeassistant/components/vesync/translations/de.json index ea05a60ff82..bd1ba32fb4a 100644 --- a/homeassistant/components/vesync/translations/de.json +++ b/homeassistant/components/vesync/translations/de.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail-Adresse" + "username": "E-Mail" }, "title": "Benutzername und Passwort eingeben" } diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json index 0e146a4159f..798410c56e3 100644 --- a/homeassistant/components/vilfo/translations/de.json +++ b/homeassistant/components/vilfo/translations/de.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "Dieser Vilfo Router ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfe den Zugriffstoken und versuche es erneut.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { - "access_token": "Zugriffstoken", + "access_token": "Zugangstoken", "host": "Host" }, "description": "Richte die Vilfo Router-Integration ein. Du ben\u00f6tigst deinen Vilfo Router-Hostnamen / deine IP-Adresse und ein API-Zugriffstoken. Weitere Informationen zu dieser Integration und wie du diese Details erh\u00e4ltst, findest du unter: https://www.home-assistant.io/integrations/vilfo", diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json index b0735db1249..debbb129459 100644 --- a/homeassistant/components/wemo/translations/de.json +++ b/homeassistant/components/wemo/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Es wurden keine Wemo-Ger\u00e4te im Netzwerk gefunden.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { diff --git a/homeassistant/components/wiffi/translations/de.json b/homeassistant/components/wiffi/translations/de.json index 4084cda8f9f..c94122cac5e 100644 --- a/homeassistant/components/wiffi/translations/de.json +++ b/homeassistant/components/wiffi/translations/de.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "port": "Server Port" + "port": "Port" }, "title": "TCP-Server f\u00fcr WIFFI-Ger\u00e4te einrichten" } diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index e97fb86d3e8..01b0839ba32 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { diff --git a/homeassistant/components/xbox/translations/de.json b/homeassistant/components/xbox/translations/de.json index 04f32e05f8b..615c8f8cf2a 100644 --- a/homeassistant/components/xbox/translations/de.json +++ b/homeassistant/components/xbox/translations/de.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode w\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index 87120f09605..bc87f461c33 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -31,7 +31,7 @@ }, "user": { "data": { - "host": "IP-Adresse", + "host": "IP-Adresse (optional)", "interface": "Die zu verwendende Netzwerkschnittstelle", "mac": "MAC-Adresse" }, diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 01a70fe88d6..23a003daa39 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -50,7 +50,7 @@ "name": "Name des Gateways", "token": "API-Token" }, - "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara Integration verwendet wird.", "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, "manual": { diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json index af053e59ec3..a0bf38b8def 100644 --- a/homeassistant/components/zoneminder/translations/de.json +++ b/homeassistant/components/zoneminder/translations/de.json @@ -23,7 +23,7 @@ "password": "Passwort", "path": "ZM-Pfad", "path_zms": "ZMS-Pfad", - "ssl": "Nutzt ein SSL-Zertifikat", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index 0a82d5b0bc7..b226a2e51e0 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Z-Wave ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", - "usb_path": "USB-Ger\u00e4t Pfad" + "usb_path": "USB-Ger\u00e4te-Pfad" }, "description": "Diese Integration wird nicht mehr gepflegt. Verwenden Sie bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" } @@ -26,7 +26,7 @@ }, "query_stage": { "dead": "Nicht erreichbar ({query_stage})", - "initializing": "Initialisiere" + "initializing": "Initialisierend" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index e33ab1f3f2d..6435453b1df 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -56,6 +56,14 @@ "config_parameter": "Wert des Konfigurationsparameters {subtype}", "node_status": "Status des Knotens", "value": "Aktueller Wert eines Z-Wave-Wertes" + }, + "trigger_type": { + "event.notification.entry_control": "Benachrichtigung zur Zugangskontrolle gesendet", + "event.notification.notification": "Benachrichtigung gesendet", + "event.value_notification.basic": "Grundlegendes CC-Ereignis auf {subtype}", + "event.value_notification.central_scene": "Zentrale Szenenaktion auf {subtype}", + "event.value_notification.scene_activation": "Szenenaktivierung auf {subtype}", + "state.node_status": "Knotenstatus ge\u00e4ndert" } }, "options": { From 596179d1800ddc11b108602d5c073af850744d36 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Jul 2021 17:12:14 -0700 Subject: [PATCH 488/818] Avoid dataclass incompat with mock spec (#53298) --- tests/components/forecast_solar/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 88f3bf9d4a4..8b9227a8d04 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -58,7 +58,7 @@ def mock_forecast_solar() -> Generator[None, MagicMock, None]: forecast_solar = forecast_solar_mock.return_value now = datetime(2021, 6, 27, 6, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE) - estimate = MagicMock(spec_set=models.Estimate) + estimate = MagicMock(spec=models.Estimate) estimate.now.return_value = now estimate.timezone = "Europe/Amsterdam" estimate.energy_production_today = 100000 From e78a62c8022623c1df8567c49344920374dc4b58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jul 2021 19:22:06 -1000 Subject: [PATCH 489/818] Fix homekit locks not being created from when setup from the UI (#53301) --- homeassistant/components/homekit/config_flow.py | 8 +++++++- homeassistant/components/homekit/strings.json | 2 +- homeassistant/components/homekit/translations/en.json | 2 +- homeassistant/components/homekit/util.py | 5 ++--- tests/components/homekit/test_config_flow.py | 8 +++++--- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 459bb050f65..1ec53079179 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT @@ -58,7 +59,12 @@ MODE_EXCLUDE = "exclude" INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE] -DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +DOMAINS_NEED_ACCESSORY_MODE = [ + CAMERA_DOMAIN, + LOCK_DOMAIN, + MEDIA_PLAYER_DOMAIN, + REMOTE_DOMAIN, +] NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN] CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 56bc5438eac..3c9671c93e2 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -43,7 +43,7 @@ "data": { "include_domains": "Domains to include" }, - "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", + "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.", "title": "Select domains to be included" }, "pairing": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index aa78c3e4adc..cee1e64ad56 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domains to include" }, - "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", + "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.", "title": "Select domains to be included" } } diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 673abc5da67..6585e9e9c4e 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -499,12 +499,11 @@ def accessory_friendly_name(hass_name, accessory): def state_needs_accessory_mode(state): """Return if the entity represented by the state must be paired in accessory mode.""" - if state.domain == CAMERA_DOMAIN: + if state.domain in (CAMERA_DOMAIN, LOCK_DOMAIN): return True return ( - state.domain == LOCK_DOMAIN - or state.domain == MEDIA_PLAYER_DOMAIN + state.domain == MEDIA_PLAYER_DOMAIN and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV or state.domain == REMOTE_DOMAIN and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & SUPPORT_ACTIVITY diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index c06e8aaa5ad..f3707f9f71e 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -144,6 +144,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): """Test we can setup a new instance and we create entries for accessory mode devices.""" hass.states.async_set("camera.one", "on") hass.states.async_set("camera.existing", "on") + hass.states.async_set("lock.new", "on") hass.states.async_set("media_player.two", "on", {"device_class": "tv"}) hass.states.async_set("remote.standard", "on") hass.states.async_set("remote.activity", "on", {"supported_features": 4}) @@ -180,7 +181,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"include_domains": ["camera", "media_player", "light", "remote"]}, + {"include_domains": ["camera", "media_player", "light", "lock", "remote"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "pairing" @@ -207,7 +208,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": ["media_player", "light", "remote"], + "include_domains": ["media_player", "light", "lock", "remote"], "include_entities": [], }, "exclude_accessory_mode": True, @@ -225,7 +226,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): # 4 - camera.one in accessory mode # 5 - media_player.two in accessory mode # 6 - remote.activity in accessory mode - assert len(mock_setup_entry.mock_calls) == 6 + # 7 - lock.new in accessory mode + assert len(mock_setup_entry.mock_calls) == 7 async def test_import(hass): From d98e580c3c4e30fe07e16b763ed2256dc0e8dc16 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 07:24:07 +0200 Subject: [PATCH 490/818] Use NamedTuple - nws (#53293) --- homeassistant/components/nws/const.py | 176 +++++++++++++------------ homeassistant/components/nws/sensor.py | 69 +++------- tests/components/nws/test_sensor.py | 23 ++-- 3 files changed, 122 insertions(+), 146 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index f82a70ea4e0..b5814613847 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,5 +1,8 @@ """Constants for National Weather Service Integration.""" +from __future__ import annotations + from datetime import timedelta +from typing import NamedTuple from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -17,7 +20,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, DEGREE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -40,11 +42,6 @@ ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" ATTR_FORECAST_DAYTIME = "daytime" -ATTR_ICON = "icon" -ATTR_LABEL = "label" -ATTR_UNIT = "unit" -ATTR_UNIT_CONVERT = "unit_convert" -ATTR_UNIT_CONVERT_METHOD = "unit_convert_method" CONDITION_CLASSES = { ATTR_CONDITION_EXCEPTIONAL: [ @@ -101,82 +98,93 @@ COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly" OBSERVATION_VALID_TIME = timedelta(minutes=20) FORECAST_VALID_TIME = timedelta(minutes=45) -SENSOR_TYPES = { - "dewpoint": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Dew Point", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_UNIT_CONVERT: TEMP_CELSIUS, - }, - "temperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Temperature", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_UNIT_CONVERT: TEMP_CELSIUS, - }, - "windChill": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Wind Chill", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_UNIT_CONVERT: TEMP_CELSIUS, - }, - "heatIndex": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Heat Index", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_UNIT_CONVERT: TEMP_CELSIUS, - }, - "relativeHumidity": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_LABEL: "Relative Humidity", - ATTR_UNIT: PERCENTAGE, - ATTR_UNIT_CONVERT: PERCENTAGE, - }, - "windSpeed": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Speed", - ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, - }, - "windGust": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust", - ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, - }, - "windDirection": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:compass-rose", - ATTR_LABEL: "Wind Direction", - ATTR_UNIT: DEGREE, - ATTR_UNIT_CONVERT: DEGREE, - }, - "barometricPressure": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, - ATTR_LABEL: "Barometric Pressure", - ATTR_UNIT: PRESSURE_PA, - ATTR_UNIT_CONVERT: PRESSURE_INHG, - }, - "seaLevelPressure": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, - ATTR_LABEL: "Sea Level Pressure", - ATTR_UNIT: PRESSURE_PA, - ATTR_UNIT_CONVERT: PRESSURE_INHG, - }, - "visibility": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:eye", - ATTR_LABEL: "Visibility", - ATTR_UNIT: LENGTH_METERS, - ATTR_UNIT_CONVERT: LENGTH_MILES, - }, + +class NWSSensorMetadata(NamedTuple): + """Sensor metadata for an individual NWS sensor.""" + + label: str + icon: str | None + device_class: str | None + unit: str + unit_convert: str + + +SENSOR_TYPES: dict[str, NWSSensorMetadata] = { + "dewpoint": NWSSensorMetadata( + "Dew Point", + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + unit=TEMP_CELSIUS, + unit_convert=TEMP_CELSIUS, + ), + "temperature": NWSSensorMetadata( + "Temperature", + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + unit=TEMP_CELSIUS, + unit_convert=TEMP_CELSIUS, + ), + "windChill": NWSSensorMetadata( + "Wind Chill", + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + unit=TEMP_CELSIUS, + unit_convert=TEMP_CELSIUS, + ), + "heatIndex": NWSSensorMetadata( + "Heat Index", + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + unit=TEMP_CELSIUS, + unit_convert=TEMP_CELSIUS, + ), + "relativeHumidity": NWSSensorMetadata( + "Relative Humidity", + icon=None, + device_class=DEVICE_CLASS_HUMIDITY, + unit=PERCENTAGE, + unit_convert=PERCENTAGE, + ), + "windSpeed": NWSSensorMetadata( + "Wind Speed", + icon="mdi:weather-windy", + device_class=None, + unit=SPEED_KILOMETERS_PER_HOUR, + unit_convert=SPEED_MILES_PER_HOUR, + ), + "windGust": NWSSensorMetadata( + "Wind Gust", + icon="mdi:weather-windy", + device_class=None, + unit=SPEED_KILOMETERS_PER_HOUR, + unit_convert=SPEED_MILES_PER_HOUR, + ), + "windDirection": NWSSensorMetadata( + "Wind Direction", + icon="mdi:compass-rose", + device_class=None, + unit=DEGREE, + unit_convert=DEGREE, + ), + "barometricPressure": NWSSensorMetadata( + "Barometric Pressure", + icon=None, + device_class=DEVICE_CLASS_PRESSURE, + unit=PRESSURE_PA, + unit_convert=PRESSURE_INHG, + ), + "seaLevelPressure": NWSSensorMetadata( + "Sea Level Pressure", + icon=None, + device_class=DEVICE_CLASS_PRESSURE, + unit=PRESSURE_PA, + unit_convert=PRESSURE_INHG, + ), + "visibility": NWSSensorMetadata( + "Visibility", + icon="mdi:eye", + device_class=None, + unit=LENGTH_METERS, + unit_convert=LENGTH_MILES, + ), } diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index bff5cdca589..8bbf6af8057 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -2,7 +2,6 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, CONF_LATITUDE, CONF_LONGITUDE, LENGTH_KILOMETERS, @@ -14,6 +13,7 @@ from homeassistant.const import ( SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow @@ -21,10 +21,6 @@ from homeassistant.util.pressure import convert as convert_pressure from . import base_unique_id from .const import ( - ATTR_ICON, - ATTR_LABEL, - ATTR_UNIT, - ATTR_UNIT_CONVERT, ATTRIBUTION, CONF_STATION, COORDINATOR_OBSERVATION, @@ -32,6 +28,7 @@ from .const import ( NWS_DATA, OBSERVATION_VALID_TIME, SENSOR_TYPES, + NWSSensorMetadata, ) PARALLEL_UPDATES = 0 @@ -43,21 +40,15 @@ async def async_setup_entry(hass, entry, async_add_entities): station = entry.data[CONF_STATION] entities = [] - for sensor_type, sensor_data in SENSOR_TYPES.items(): - if hass.config.units.is_metric: - unit = sensor_data[ATTR_UNIT] - else: - unit = sensor_data[ATTR_UNIT_CONVERT] + for sensor_type, metadata in SENSOR_TYPES.items(): entities.append( NWSSensor( + hass, entry.data, hass_data, sensor_type, + metadata, station, - sensor_data[ATTR_LABEL], - sensor_data[ATTR_ICON], - sensor_data[ATTR_DEVICE_CLASS], - unit, ), ) @@ -69,14 +60,12 @@ class NWSSensor(CoordinatorEntity, SensorEntity): def __init__( self, + hass: HomeAssistant, entry_data, hass_data, sensor_type, + metadata: NWSSensorMetadata, station, - label, - icon, - device_class, - unit, ): """Initialise the platform with a data instance.""" super().__init__(hass_data[COORDINATOR_OBSERVATION]) @@ -84,11 +73,15 @@ class NWSSensor(CoordinatorEntity, SensorEntity): self._latitude = entry_data[CONF_LATITUDE] self._longitude = entry_data[CONF_LONGITUDE] self._type = sensor_type - self._station = station - self._label = label - self._icon = icon - self._device_class = device_class - self._unit = unit + self._metadata = metadata + + self._attr_name = f"{station} {metadata.label}" + self._attr_icon = metadata.icon + self._attr_device_class = metadata.device_class + if hass.config.units.is_metric: + self._attr_unit_of_measurement = metadata.unit + else: + self._attr_unit_of_measurement = metadata.unit_convert @property def state(self): @@ -96,43 +89,23 @@ class NWSSensor(CoordinatorEntity, SensorEntity): value = self._nws.observation.get(self._type) if value is None: return None - if self._unit == SPEED_MILES_PER_HOUR: + if self._attr_unit_of_measurement == SPEED_MILES_PER_HOUR: return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) - if self._unit == LENGTH_MILES: + if self._attr_unit_of_measurement == LENGTH_MILES: return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) - if self._unit == PRESSURE_INHG: + if self._attr_unit_of_measurement == PRESSURE_INHG: return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) - if self._unit == TEMP_CELSIUS: + if self._attr_unit_of_measurement == TEMP_CELSIUS: return round(value, 1) - if self._unit == PERCENTAGE: + if self._attr_unit_of_measurement == PERCENTAGE: return round(value) return value - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - @property def device_state_attributes(self): """Return the attribution.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - @property - def name(self): - """Return the name of the station.""" - return f"{self._station} {self._label}" - @property def unique_id(self): """Return a unique_id for this entity.""" diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py index 44b181b1ec4..f5c0773380d 100644 --- a/tests/components/nws/test_sensor.py +++ b/tests/components/nws/test_sensor.py @@ -1,12 +1,7 @@ """Sensors for National Weather Service (NWS).""" import pytest -from homeassistant.components.nws.const import ( - ATTR_LABEL, - ATTRIBUTION, - DOMAIN, - SENSOR_TYPES, -) +from homeassistant.components.nws.const import ATTRIBUTION, DOMAIN, SENSOR_TYPES from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN from homeassistant.util import slugify @@ -40,12 +35,12 @@ async def test_imperial_metric( """Test with imperial and metric units.""" registry = await hass.helpers.entity_registry.async_get_registry() - for sensor_name, sensor_data in SENSOR_TYPES.items(): + for sensor_name, metadata in SENSOR_TYPES.items(): registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, f"35_-75_{sensor_name}", - suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}", + suggested_object_id=f"abc_{metadata.label}", disabled_by=None, ) @@ -58,8 +53,8 @@ async def test_imperial_metric( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for sensor_name, sensor_data in SENSOR_TYPES.items(): - state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}") + for sensor_name, metadata in SENSOR_TYPES.items(): + state = hass.states.get(f"sensor.abc_{slugify(metadata.label)}") assert state assert state.state == result_observation[sensor_name] assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -72,12 +67,12 @@ async def test_none_values(hass, mock_simple_nws, no_weather): registry = await hass.helpers.entity_registry.async_get_registry() - for sensor_name, sensor_data in SENSOR_TYPES.items(): + for sensor_name, metadata in SENSOR_TYPES.items(): registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, f"35_-75_{sensor_name}", - suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}", + suggested_object_id=f"abc_{metadata.label}", disabled_by=None, ) @@ -89,7 +84,7 @@ async def test_none_values(hass, mock_simple_nws, no_weather): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for sensor_name, sensor_data in SENSOR_TYPES.items(): - state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}") + for sensor_name, metadata in SENSOR_TYPES.items(): + state = hass.states.get(f"sensor.abc_{slugify(metadata.label)}") assert state assert state.state == STATE_UNKNOWN From f5480481cd29e7dd1db71a6be596abaa70288409 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 07:25:38 +0200 Subject: [PATCH 491/818] Use NamedTuple - metoffice (#53294) --- homeassistant/components/metoffice/sensor.py | 195 ++++++++++++------- 1 file changed, 122 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 6b45dac22e7..1307c3aae45 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,4 +1,8 @@ """Support for UK Met Office weather service.""" +from __future__ import annotations + +from typing import NamedTuple + from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -34,51 +38,102 @@ ATTR_SENSOR_ID = "sensor_id" ATTR_SITE_ID = "site_id" ATTR_SITE_NAME = "site_name" -# Sensor types are defined as: -# variable -> [0]title, [1]device_class, [2]units, [3]icon, [4]enabled_by_default + +class MetOfficeSensorMetadata(NamedTuple): + """Sensor metadata for an individual NWS sensor.""" + + title: str + device_class: str | None + unit_of_measurement: str | None + icon: str | None + enabled_by_default: bool + + SENSOR_TYPES = { - "name": ["Station Name", None, None, "mdi:label-outline", False], - "weather": [ + "name": MetOfficeSensorMetadata( + "Station Name", + device_class=None, + unit_of_measurement=None, + icon="mdi:label-outline", + enabled_by_default=False, + ), + "weather": MetOfficeSensorMetadata( "Weather", - None, - None, - "mdi:weather-sunny", # but will adapt to current conditions - True, - ], - "temperature": ["Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None, True], - "feels_like_temperature": [ + device_class=None, + unit_of_measurement=None, + icon="mdi:weather-sunny", # but will adapt to current conditions + enabled_by_default=True, + ), + "temperature": MetOfficeSensorMetadata( + "Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + icon=None, + enabled_by_default=True, + ), + "feels_like_temperature": MetOfficeSensorMetadata( "Feels Like Temperature", - DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, - None, - False, - ], - "wind_speed": [ + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + icon=None, + enabled_by_default=False, + ), + "wind_speed": MetOfficeSensorMetadata( "Wind Speed", - None, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - True, - ], - "wind_direction": ["Wind Direction", None, None, "mdi:compass-outline", False], - "wind_gust": ["Wind Gust", None, SPEED_MILES_PER_HOUR, "mdi:weather-windy", False], - "visibility": ["Visibility", None, None, "mdi:eye", False], - "visibility_distance": [ + device_class=None, + unit_of_measurement=SPEED_MILES_PER_HOUR, + icon="mdi:weather-windy", + enabled_by_default=True, + ), + "wind_direction": MetOfficeSensorMetadata( + "Wind Direction", + device_class=None, + unit_of_measurement=None, + icon="mdi:compass-outline", + enabled_by_default=False, + ), + "wind_gust": MetOfficeSensorMetadata( + "Wind Gust", + device_class=None, + unit_of_measurement=SPEED_MILES_PER_HOUR, + icon="mdi:weather-windy", + enabled_by_default=False, + ), + "visibility": MetOfficeSensorMetadata( + "Visibility", + device_class=None, + unit_of_measurement=None, + icon="mdi:eye", + enabled_by_default=False, + ), + "visibility_distance": MetOfficeSensorMetadata( "Visibility Distance", - None, - LENGTH_KILOMETERS, - "mdi:eye", - False, - ], - "uv": ["UV Index", None, UV_INDEX, "mdi:weather-sunny-alert", True], - "precipitation": [ + device_class=None, + unit_of_measurement=LENGTH_KILOMETERS, + icon="mdi:eye", + enabled_by_default=False, + ), + "uv": MetOfficeSensorMetadata( + "UV Index", + device_class=None, + unit_of_measurement=UV_INDEX, + icon="mdi:weather-sunny-alert", + enabled_by_default=True, + ), + "precipitation": MetOfficeSensorMetadata( "Probability of Precipitation", - None, - PERCENTAGE, - "mdi:weather-rainy", - True, - ], - "humidity": ["Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE, None, False], + device_class=None, + unit_of_measurement=PERCENTAGE, + icon="mdi:weather-rainy", + enabled_by_default=True, + ), + "humidity": MetOfficeSensorMetadata( + "Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + unit_of_measurement=PERCENTAGE, + icon=None, + enabled_by_default=False, + ), } @@ -91,15 +146,23 @@ async def async_setup_entry( async_add_entities( [ MetOfficeCurrentSensor( - hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, True, sensor_type + hass_data[METOFFICE_HOURLY_COORDINATOR], + hass_data, + True, + sensor_type, + metadata, ) - for sensor_type in SENSOR_TYPES + for sensor_type, metadata in SENSOR_TYPES.items() ] + [ MetOfficeCurrentSensor( - hass_data[METOFFICE_DAILY_COORDINATOR], hass_data, False, sensor_type + hass_data[METOFFICE_DAILY_COORDINATOR], + hass_data, + False, + sensor_type, + metadata, ) - for sensor_type in SENSOR_TYPES + for sensor_type, metadata in SENSOR_TYPES.items() ], False, ) @@ -108,33 +171,29 @@ async def async_setup_entry( class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): """Implementation of a Met Office current weather condition sensor.""" - def __init__(self, coordinator, hass_data, use_3hourly, sensor_type): + def __init__( + self, + coordinator, + hass_data, + use_3hourly, + sensor_type, + metadata: MetOfficeSensorMetadata, + ): """Initialize the sensor.""" super().__init__(coordinator) self._type = sensor_type + self._metadata = metadata mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL - self._name = ( - f"{hass_data[METOFFICE_NAME]} {SENSOR_TYPES[self._type][0]} {mode_label}" - ) - self._unique_id = ( - f"{SENSOR_TYPES[self._type][0]}_{hass_data[METOFFICE_COORDINATES]}" - ) + self._attr_name = f"{hass_data[METOFFICE_NAME]} {metadata.title} {mode_label}" + self._attr_unique_id = f"{metadata.title}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: - self._unique_id = f"{self._unique_id}_{MODE_DAILY}" + self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" + self._attr_device_class = metadata.device_class + self._attr_unit_of_measurement = metadata.unit_of_measurement self.use_3hourly = use_3hourly - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique of the sensor.""" - return self._unique_id - @property def state(self): """Return the state of the sensor.""" @@ -167,15 +226,10 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return value - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return SENSOR_TYPES[self._type][2] - @property def icon(self): """Return the icon for the entity card.""" - value = SENSOR_TYPES[self._type][3] + value = self._metadata.icon if self._type == "weather": value = self.state if value is None: @@ -186,11 +240,6 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return value - @property - def device_class(self): - """Return the device class of the sensor.""" - return SENSOR_TYPES[self._type][1] - @property def extra_state_attributes(self): """Return the state attributes of the device.""" @@ -205,4 +254,4 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return SENSOR_TYPES[self._type][4] and self.use_3hourly + return self._metadata.enabled_by_default and self.use_3hourly From 5c3fb77660b76285fa379becf4e894664a9bb6f5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 07:27:01 +0200 Subject: [PATCH 492/818] Use NamedTuple - glances (#53297) --- homeassistant/components/glances/const.py | 194 ++++++++++++++++----- homeassistant/components/glances/sensor.py | 57 ++---- 2 files changed, 165 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 8e20fbfa46b..56d55931cdb 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,5 +1,8 @@ """Constants for Glances component.""" +from __future__ import annotations + import sys +from typing import NamedTuple from homeassistant.const import ( DATA_GIBIBYTES, @@ -26,51 +29,148 @@ if sys.maxsize > 2 ** 32: else: CPU_ICON = "mdi:cpu-32-bit" -SENSOR_TYPES = { - "disk_use_percent": ["fs", "used percent", PERCENTAGE, "mdi:harddisk", None], - "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk", None], - "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk", None], - "memory_use_percent": ["mem", "RAM used percent", PERCENTAGE, "mdi:memory", None], - "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory", None], - "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory", None], - "swap_use_percent": [ - "memswap", - "Swap used percent", - PERCENTAGE, - "mdi:memory", - None, - ], - "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory", None], - "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory", None], - "processor_load": ["load", "CPU load", "15 min", CPU_ICON, None], - "process_running": ["processcount", "Running", "Count", CPU_ICON, None], - "process_total": ["processcount", "Total", "Count", CPU_ICON, None], - "process_thread": ["processcount", "Thread", "Count", CPU_ICON, None], - "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON, None], - "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON, None], - "temperature_core": [ - "sensors", - "Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "temperature_hdd": [ - "sensors", - "Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "fan_speed": ["sensors", "Fan speed", "RPM", "mdi:fan", None], - "battery": ["sensors", "Charge", PERCENTAGE, "mdi:battery", None], - "docker_active": ["docker", "Containers active", "", "mdi:docker", None], - "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker", None], - "docker_memory_use": [ - "docker", - "Containers RAM used", - DATA_MEBIBYTES, - "mdi:docker", - None, - ], + +class GlancesSensorMetadata(NamedTuple): + """Sensor metadata for an individual Glances sensor.""" + + type: str + name_suffix: str + unit_of_measurement: str + icon: str | None = None + device_class: str | None = None + + +SENSOR_TYPES: dict[str, GlancesSensorMetadata] = { + "disk_use_percent": GlancesSensorMetadata( + type="fs", + name_suffix="used percent", + unit_of_measurement=PERCENTAGE, + icon="mdi:harddisk", + ), + "disk_use": GlancesSensorMetadata( + type="fs", + name_suffix="used", + unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:harddisk", + ), + "disk_free": GlancesSensorMetadata( + type="fs", + name_suffix="free", + unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:harddisk", + ), + "memory_use_percent": GlancesSensorMetadata( + type="mem", + name_suffix="RAM used percent", + unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + ), + "memory_use": GlancesSensorMetadata( + type="mem", + name_suffix="RAM used", + unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:memory", + ), + "memory_free": GlancesSensorMetadata( + type="mem", + name_suffix="RAM free", + unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:memory", + ), + "swap_use_percent": GlancesSensorMetadata( + type="memswap", + name_suffix="Swap used percent", + unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + ), + "swap_use": GlancesSensorMetadata( + type="memswap", + name_suffix="Swap used", + unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:memory", + ), + "swap_free": GlancesSensorMetadata( + type="memswap", + name_suffix="Swap free", + unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:memory", + ), + "processor_load": GlancesSensorMetadata( + type="load", + name_suffix="CPU load", + unit_of_measurement="15 min", + icon=CPU_ICON, + ), + "process_running": GlancesSensorMetadata( + type="processcount", + name_suffix="Running", + unit_of_measurement="Count", + icon=CPU_ICON, + ), + "process_total": GlancesSensorMetadata( + type="processcount", + name_suffix="Total", + unit_of_measurement="Count", + icon=CPU_ICON, + ), + "process_thread": GlancesSensorMetadata( + type="processcount", + name_suffix="Thread", + unit_of_measurement="Count", + icon=CPU_ICON, + ), + "process_sleeping": GlancesSensorMetadata( + type="processcount", + name_suffix="Sleeping", + unit_of_measurement="Count", + icon=CPU_ICON, + ), + "cpu_use_percent": GlancesSensorMetadata( + type="cpu", + name_suffix="CPU used", + unit_of_measurement=PERCENTAGE, + icon=CPU_ICON, + ), + "temperature_core": GlancesSensorMetadata( + type="sensors", + name_suffix="Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + "temperature_hdd": GlancesSensorMetadata( + type="sensors", + name_suffix="Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + "fan_speed": GlancesSensorMetadata( + type="sensors", + name_suffix="Fan speed", + unit_of_measurement="RPM", + icon="mdi:fan", + ), + "battery": GlancesSensorMetadata( + type="sensors", + name_suffix="Charge", + unit_of_measurement=PERCENTAGE, + icon="mdi:battery", + ), + "docker_active": GlancesSensorMetadata( + type="docker", + name_suffix="Containers active", + unit_of_measurement="", + icon="mdi:docker", + ), + "docker_cpu_use": GlancesSensorMetadata( + type="docker", + name_suffix="Containers CPU used", + unit_of_measurement=PERCENTAGE, + icon="mdi:docker", + ), + "docker_memory_use": GlancesSensorMetadata( + type="docker", + name_suffix="Containers RAM used", + unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:docker", + ), } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 0e032de67be..e33fd121200 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -4,7 +4,7 @@ from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES +from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorMetadata async def async_setup_entry(hass, config_entry, async_add_entities): @@ -14,45 +14,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = config_entry.data[CONF_NAME] dev = [] - for sensor_type, sensor_details in SENSOR_TYPES.items(): - if sensor_details[0] not in client.api.data: + for sensor_type, metadata in SENSOR_TYPES.items(): + if metadata.type not in client.api.data: continue - if sensor_details[0] == "fs": + if metadata.type == "fs": # fs will provide a list of disks attached - for disk in client.api.data[sensor_details[0]]: + for disk in client.api.data[metadata.type]: dev.append( GlancesSensor( client, name, disk["mnt_point"], - sensor_details[1], sensor_type, - sensor_details, + metadata, ) ) - elif sensor_details[0] == "sensors": + elif metadata.type == "sensors": # sensors will provide temp for different devices - for sensor in client.api.data[sensor_details[0]]: + for sensor in client.api.data[metadata.type]: if sensor["type"] == sensor_type: dev.append( GlancesSensor( client, name, sensor["label"], - sensor_details[1], sensor_type, - sensor_details, + metadata, ) ) - elif client.api.data[sensor_details[0]]: + elif client.api.data[metadata.type]: dev.append( GlancesSensor( client, name, "", - sensor_details[1], sensor_type, - sensor_details, + metadata, ) ) @@ -67,45 +64,27 @@ class GlancesSensor(SensorEntity): glances_data, name, sensor_name_prefix, - sensor_name_suffix, sensor_type, - sensor_details, + metadata: GlancesSensorMetadata, ): """Initialize the sensor.""" self.glances_data = glances_data self._sensor_name_prefix = sensor_name_prefix - self._sensor_name_suffix = sensor_name_suffix - self._name = name self.type = sensor_type self._state = None - self.sensor_details = sensor_details + self._metadata = metadata self.unsub_update = None - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._sensor_name_prefix} {self._sensor_name_suffix}" + self._attr_name = f"{name} {sensor_name_prefix} {metadata.name_suffix}" + self._attr_icon = metadata.icon + self._attr_unit_of_measurement = metadata.unit_of_measurement + self._attr_device_class = metadata.device_class @property def unique_id(self): """Set unique_id for sensor.""" return f"{self.glances_data.host}-{self.name}" - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self.sensor_details[4] - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self.sensor_details[3] - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self.sensor_details[2] - @property def available(self): """Could the device be accessed during the last update call.""" @@ -143,7 +122,7 @@ class GlancesSensor(SensorEntity): if value is None: return - if self.sensor_details[0] == "fs": + if self._metadata.type == "fs": for var in value["fs"]: if var["mnt_point"] == self._sensor_name_prefix: disk = var From 551c1177172850a9fb6afbed3bf99c1740fb470b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 07:27:31 +0200 Subject: [PATCH 493/818] Use NamedTuple - ondilo_ico (#53296) --- homeassistant/components/ondilo_ico/sensor.py | 102 ++++++++++-------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 633e03157e4..122b4154892 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -1,6 +1,9 @@ """Platform for sensor integration.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import NamedTuple from ondilo import OndiloError @@ -22,29 +25,59 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN -SENSOR_TYPES = { - "temperature": [ + +class OndiloIOCSensorMetadata(NamedTuple): + """Sensor metadata for an individual Ondilo IOC sensor.""" + + name: str + unit_of_measurement: str | None + icon: str | None + device_class: str | None + + +SENSOR_TYPES: dict[str, OndiloIOCSensorMetadata] = { + "temperature": OndiloIOCSensorMetadata( "Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "orp": [ + unit_of_measurement=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + "orp": OndiloIOCSensorMetadata( "Oxydo Reduction Potential", - ELECTRIC_POTENTIAL_MILLIVOLT, - "mdi:pool", - None, - ], - "ph": ["pH", "", "mdi:pool", None], - "tds": ["TDS", CONCENTRATION_PARTS_PER_MILLION, "mdi:pool", None], - "battery": ["Battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY], - "rssi": [ + unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + icon="mdi:pool", + device_class=None, + ), + "ph": OndiloIOCSensorMetadata( + "pH", + unit_of_measurement=None, + icon="mdi:pool", + device_class=None, + ), + "tds": OndiloIOCSensorMetadata( + "TDS", + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + icon="mdi:pool", + device_class=None, + ), + "battery": OndiloIOCSensorMetadata( + "Battery", + unit_of_measurement=PERCENTAGE, + icon=None, + device_class=DEVICE_CLASS_BATTERY, + ), + "rssi": OndiloIOCSensorMetadata( "RSSI", - PERCENTAGE, - None, - DEVICE_CLASS_SIGNAL_STRENGTH, - ], - "salt": ["Salt", "mg/L", "mdi:pool", None], + unit_of_measurement=PERCENTAGE, + icon=None, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + ), + "salt": OndiloIOCSensorMetadata( + "Salt", + unit_of_measurement="mg/L", + icon="mdi:pool", + device_class=None, + ), } SCAN_INTERVAL = timedelta(hours=1) @@ -105,10 +138,11 @@ class OndiloICO(CoordinatorEntity, SensorEntity): self._data_type = pooldata["sensors"][sensor_idx]["data_type"] self._unique_id = f"{pooldata['ICO']['serial_number']}-{self._data_type}" self._device_name = pooldata["name"] - self._name = f"{self._device_name} {SENSOR_TYPES[self._data_type][0]}" - self._device_class = SENSOR_TYPES[self._data_type][3] - self._icon = SENSOR_TYPES[self._data_type][2] - self._unit = SENSOR_TYPES[self._data_type][1] + metadata = SENSOR_TYPES[self._data_type] + self._name = f"{self._device_name} {metadata.name}" + self._attr_device_class = metadata.device_class + self._attr_icon = metadata.icon + self._attr_unit_of_measurement = metadata.unit_of_measurement def _pooldata(self): """Get pool data dict.""" @@ -128,31 +162,11 @@ class OndiloICO(CoordinatorEntity, SensorEntity): None, ) - @property - def name(self): - """Name of the sensor.""" - return self._name - @property def state(self): """Last value of the sensor.""" return self._devdata()["value"] - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the Unit of the sensor's measurement.""" - return self._unit - @property def unique_id(self): """Return the unique ID of this entity.""" From 560bde94efb43dfbbb8761fc665ebe9bb30edd01 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 07:28:02 +0200 Subject: [PATCH 494/818] Use NamedTuple - epsonworkforce (#53295) --- .../components/epsonworkforce/sensor.py | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 22f74e1c0b1..65a5e6342f1 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -1,5 +1,8 @@ """Support for Epson Workforce Printer.""" +from __future__ import annotations + from datetime import timedelta +from typing import NamedTuple from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol @@ -9,13 +12,46 @@ from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -MONITORED_CONDITIONS = { - "black": ["Ink level Black", PERCENTAGE, "mdi:water"], - "photoblack": ["Ink level Photoblack", PERCENTAGE, "mdi:water"], - "magenta": ["Ink level Magenta", PERCENTAGE, "mdi:water"], - "cyan": ["Ink level Cyan", PERCENTAGE, "mdi:water"], - "yellow": ["Ink level Yellow", PERCENTAGE, "mdi:water"], - "clean": ["Cleaning level", PERCENTAGE, "mdi:water"], + +class MonitoredConditionsMetadata(NamedTuple): + """Metadata for an individual montiored condition.""" + + name: str + icon: str + unit_of_measurement: str + + +MONITORED_CONDITIONS: dict[str, MonitoredConditionsMetadata] = { + "black": MonitoredConditionsMetadata( + "Ink level Black", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + "photoblack": MonitoredConditionsMetadata( + "Ink level Photoblack", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + "magenta": MonitoredConditionsMetadata( + "Ink level Magenta", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + "cyan": MonitoredConditionsMetadata( + "Ink level Cyan", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + "yellow": MonitoredConditionsMetadata( + "Ink level Yellow", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + "clean": MonitoredConditionsMetadata( + "Cleaning level", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -52,24 +88,10 @@ class EpsonPrinterCartridge(SensorEntity): self._api = api self._id = cartridgeidx - self._name = MONITORED_CONDITIONS[self._id][0] - self._unit = MONITORED_CONDITIONS[self._id][1] - self._icon = MONITORED_CONDITIONS[self._id][2] - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit + metadata = MONITORED_CONDITIONS[self._id] + self._attr_name = metadata.name + self._attr_icon = metadata.icon + self._attr_unit_of_measurement = metadata.unit_of_measurement @property def state(self): From 1bde91407508ca600c5c0f905140f9596731e82a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 22 Jul 2021 00:01:05 -0600 Subject: [PATCH 495/818] Ensure Guardian is strictly typed (#53253) --- .strict-typing | 1 + homeassistant/components/guardian/__init__.py | 51 +++++++++++-------- .../components/guardian/binary_sensor.py | 2 +- .../components/guardian/config_flow.py | 4 +- .../components/guardian/manifest.json | 2 +- homeassistant/components/guardian/sensor.py | 2 +- homeassistant/components/guardian/switch.py | 24 +++++---- homeassistant/components/guardian/util.py | 6 +-- mypy.ini | 14 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/mypy_config.py | 1 - 12 files changed, 67 insertions(+), 44 deletions(-) diff --git a/.strict-typing b/.strict-typing index 98a96f98fb0..19835bfd777 100644 --- a/.strict-typing +++ b/.strict-typing @@ -43,6 +43,7 @@ homeassistant.components.fritz.* homeassistant.components.geo_location.* homeassistant.components.gios.* homeassistant.components.group.* +homeassistant.components.guardian.* homeassistant.components.history.* homeassistant.components.homeassistant.triggers.event homeassistant.components.http.* diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 8e605ca121c..96f5ed36720 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, MutableMapping +from typing import Any, cast from aioguardian import Client @@ -89,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await paired_sensor_manager.async_process_latest_paired_sensor_uids() @callback - def async_process_paired_sensor_uids(): + def async_process_paired_sensor_uids() -> None: """Define a callback for when new paired sensor data is received.""" hass.async_create_task( paired_sensor_manager.async_process_latest_paired_sensor_uids() @@ -133,8 +135,7 @@ class PairedSensorManager: self._client = client self._entry = entry self._hass = hass - self._listeners = [] - self._paired_uids = set() + self._paired_uids: set[str] = set() async def async_pair_sensor(self, uid: str) -> None: """Add a new paired sensor coordinator.""" @@ -148,7 +149,9 @@ class PairedSensorManager: self._hass, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", - api_coro=lambda: self._client.sensor.paired_sensor_status(uid), + api_coro=lambda: cast( + Awaitable, self._client.sensor.paired_sensor_status(uid) + ), api_lock=self._api_lock, valve_controller_uid=self._entry.data[CONF_UID], ) @@ -208,12 +211,19 @@ class GuardianEntity(CoordinatorEntity): """Define a base Guardian entity.""" def __init__( # pylint: disable=super-init-not-called - self, entry: ConfigEntry, kind: str, name: str, device_class: str, icon: str + self, + entry: ConfigEntry, + kind: str, + name: str, + device_class: str | None, + icon: str | None, ) -> None: """Initialize.""" self._attr_device_class = device_class self._attr_device_info = {"manufacturer": "Elexa"} - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: "Data provided by Elexa"} + self._attr_extra_state_attributes: MutableMapping[str, Any] = { + ATTR_ATTRIBUTION: "Data provided by Elexa" + } self._attr_icon = icon self._attr_name = name self._entry = entry @@ -236,16 +246,18 @@ class PairedSensorEntity(GuardianEntity): coordinator: DataUpdateCoordinator, kind: str, name: str, - device_class: str, - icon: str, + device_class: str | None, + icon: str | None, ) -> None: """Initialize.""" super().__init__(entry, kind, name, device_class, icon) paired_sensor_uid = coordinator.data["uid"] - self._attr_device_info["identifiers"] = {(DOMAIN, paired_sensor_uid)} - self._attr_device_info["name"] = f"Guardian Paired Sensor {paired_sensor_uid}" - self._attr_device_info["via_device"] = (DOMAIN, entry.data[CONF_UID]) + self._attr_device_info = { + "identifiers": {(DOMAIN, paired_sensor_uid)}, + "name": f"Guardian Paired Sensor {paired_sensor_uid}", + "via_device": (DOMAIN, entry.data[CONF_UID]), + } self._attr_name = f"Guardian Paired Sensor {paired_sensor_uid}: {name}" self._attr_unique_id = f"{paired_sensor_uid}_{kind}" self._kind = kind @@ -271,13 +283,11 @@ class ValveControllerEntity(GuardianEntity): """Initialize.""" super().__init__(entry, kind, name, device_class, icon) - self._attr_device_info["identifiers"] = {(DOMAIN, entry.data[CONF_UID])} - self._attr_device_info[ - "name" - ] = f"Guardian Valve Controller {entry.data[CONF_UID]}" - self._attr_device_info["model"] = coordinators[API_SYSTEM_DIAGNOSTICS].data[ - "firmware" - ] + self._attr_device_info = { + "identifiers": {(DOMAIN, entry.data[CONF_UID])}, + "name": f"Guardian Valve Controller {entry.data[CONF_UID]}", + "model": coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], + } self._attr_name = f"Guardian {entry.data[CONF_UID]}: {name}" self._attr_unique_id = f"{entry.data[CONF_UID]}_{kind}" self._kind = kind @@ -304,7 +314,7 @@ class ValveControllerEntity(GuardianEntity): """Add a listener to a DataUpdateCoordinator based on the API referenced.""" @callback - def update(): + def update() -> None: """Update the entity's state.""" self._async_update_from_latest_data() self.async_write_ha_state() @@ -327,6 +337,7 @@ class ValveControllerEntity(GuardianEntity): return refresh_tasks = [ - coordinator.async_request_refresh() for coordinator in self.coordinators + coordinator.async_request_refresh() + for coordinator in self.coordinators.values() ] await asyncio.gather(*refresh_tasks) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 7d38a431f4c..8ce381e0456 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -78,7 +78,7 @@ async def async_setup_entry( ) ) - sensors = [] + sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [] # Add all valve controller-specific binary sensors: for kind in VALVE_CONTROLLER_SENSORS: diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index edbf4ef9c83..ccebeb99675 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -40,7 +40,7 @@ def async_get_pin_from_uid(uid: str) -> str: return uid[-4:] -async def validate_input(hass: HomeAssistant, data: dict[str, Any]): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -60,7 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" - self.discovery_info = {} + self.discovery_info: dict[str, Any] = {} async def _async_set_unique_id(self, pin: str) -> None: """Set the config entry's unique ID (based on the device's 4-digit PIN).""" diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 60411c5292b..baa7eb50e7a 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==1.0.4"], + "requirements": ["aioguardian==1.0.8"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 1aaf83b8fb8..ce09bb99c60 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -78,7 +78,7 @@ async def async_setup_entry( ) ) - sensors = [] + sensors: list[PairedSensorSensor | ValveControllerSensor] = [] # Add all valve controller-specific binary sensors: for kind in VALVE_CONTROLLER_SENSORS: diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index ef74a35147f..f3621a72952 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,6 +1,8 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations +from typing import Any + from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol @@ -95,7 +97,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): self._attr_is_on = True self._client = client - async def _async_continue_entity_setup(self): + async def _async_continue_entity_setup(self) -> None: """Register API interest (and related tasks) when the entity is added.""" self.async_add_coordinator_update_listener(API_VALVE_STATUS) @@ -127,7 +129,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): } ) - async def async_disable_ap(self): + async def async_disable_ap(self) -> None: """Disable the device's onboard access point.""" try: async with self._client: @@ -135,7 +137,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): except GuardianError as err: LOGGER.error("Error while disabling valve controller AP: %s", err) - async def async_enable_ap(self): + async def async_enable_ap(self) -> None: """Enable the device's onboard access point.""" try: async with self._client: @@ -143,7 +145,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): except GuardianError as err: LOGGER.error("Error while enabling valve controller AP: %s", err) - async def async_pair_sensor(self, *, uid): + async def async_pair_sensor(self, *, uid: str) -> None: """Add a new paired sensor.""" try: async with self._client: @@ -156,7 +158,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): self._entry.entry_id ].async_pair_sensor(uid) - async def async_reboot(self): + async def async_reboot(self) -> None: """Reboot the device.""" try: async with self._client: @@ -164,7 +166,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): except GuardianError as err: LOGGER.error("Error while rebooting valve controller: %s", err) - async def async_reset_valve_diagnostics(self): + async def async_reset_valve_diagnostics(self) -> None: """Fully reset system motor diagnostics.""" try: async with self._client: @@ -172,7 +174,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): except GuardianError as err: LOGGER.error("Error while resetting valve diagnostics: %s", err) - async def async_unpair_sensor(self, *, uid): + async def async_unpair_sensor(self, *, uid: str) -> None: """Add a new paired sensor.""" try: async with self._client: @@ -185,7 +187,9 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): self._entry.entry_id ].async_unpair_sensor(uid) - async def async_upgrade_firmware(self, *, url, port, filename): + async def async_upgrade_firmware( + self, *, url: str, port: int, filename: str + ) -> None: """Upgrade the device firmware.""" try: async with self._client: @@ -197,7 +201,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): except GuardianError as err: LOGGER.error("Error while upgrading firmware: %s", err) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: dict[str, Any]) -> None: """Turn the valve off (closed).""" try: async with self._client: @@ -209,7 +213,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): self._attr_is_on = False self.async_write_ha_state() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: dict[str, Any]) -> None: """Turn the valve on (open).""" try: async with self._client: diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index beaf71dea51..c4d0e0be4d7 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable from datetime import timedelta -from typing import Callable +from typing import Any, Callable, Dict, cast from aioguardian import Client from aioguardian.errors import GuardianError @@ -42,11 +42,11 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_lock = api_lock self._client = client - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> dict[str, Any]: """Execute a "locked" API request against the valve controller.""" async with self._api_lock, self._client: try: resp = await self._api_coro() except GuardianError as err: raise UpdateFailed(err) from err - return resp["data"] + return cast(Dict[str, Any], resp["data"]) diff --git a/mypy.ini b/mypy.ini index 32fff8d5105..e0bdc372f50 100644 --- a/mypy.ini +++ b/mypy.ini @@ -484,6 +484,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.guardian.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.history.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1252,9 +1263,6 @@ ignore_errors = true [mypy-homeassistant.components.gtfs.*] ignore_errors = true -[mypy-homeassistant.components.guardian.*] -ignore_errors = true - [mypy-homeassistant.components.habitica.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index 927a685ded4..fa3b4d2ba4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioflo==0.4.1 aioftp==0.12.0 # homeassistant.components.guardian -aioguardian==1.0.4 +aioguardian==1.0.8 # homeassistant.components.harmony aioharmony==0.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 101e0bbe4b0..eb34d203904 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioesphomeapi==5.0.1 aioflo==0.4.1 # homeassistant.components.guardian -aioguardian==1.0.4 +aioguardian==1.0.8 # homeassistant.components.harmony aioharmony==0.2.7 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 8dff2c8f89f..b1c74fceb45 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -61,7 +61,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.gree.*", "homeassistant.components.growatt_server.*", "homeassistant.components.gtfs.*", - "homeassistant.components.guardian.*", "homeassistant.components.habitica.*", "homeassistant.components.harmony.*", "homeassistant.components.hassio.*", From b9a6ce77d1602bfc365b24aac99deb38fa855048 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 22 Jul 2021 02:37:10 -0400 Subject: [PATCH 496/818] Bump zwave-js-server-python to 0.28.0 (#53302) --- homeassistant/components/zwave_js/const.py | 4 ---- homeassistant/components/zwave_js/entity.py | 8 +++---- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/siren.py | 7 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_services.py | 13 +++++----- tests/components/zwave_js/test_siren.py | 1 + .../zwave_js/aeotec_zw164_siren_state.json | 24 ++++++++++++------- .../zwave_js/bulb_6_multi_color_state.json | 15 ++++++++---- 10 files changed, 45 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 4687110e208..ae5607745f6 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -70,7 +70,3 @@ ATTR_BROADCAST = "broadcast" SERVICE_PING = "ping" ADDON_SLUG = "core_zwave_js" - -# Siren constants -TONE_ID_DEFAULT = 255 -TONE_ID_OFF = 0 diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 432bc2fa868..6df7c8d546b 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.model.node import NodeStatus +from zwave_js_server.const import NodeStatus from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry @@ -213,13 +213,13 @@ class ZWaveBaseEntity(Entity): # If we haven't found a value and check_all_endpoints is True, we should # return the first value we can find on any other endpoint if return_value is None and check_all_endpoints: - for endpoint_ in self.info.node.endpoints: - if endpoint_.index != self.info.primary_value.endpoint: + for endpoint_idx in self.info.node.endpoints: + if endpoint_idx != self.info.primary_value.endpoint: value_id = get_value_id( self.info.node, command_class, value_property, - endpoint=endpoint_.index, + endpoint=endpoint_idx, property_key=value_property_key, ) return_value = self.info.node.values.get(value_id) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index d719e3976a4..b24bc957303 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.27.1"], + "requirements": ["zwave-js-server-python==0.28.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index fa6e24878ed..de74f55fa9a 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import ToneID from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN, SirenEntity from homeassistant.components.siren.const import ( @@ -19,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DOMAIN, TONE_ID_DEFAULT, TONE_ID_OFF +from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -87,7 +88,7 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): options["volume"] = round(volume * 100) # Play the default tone if a tone isn't provided if tone is None: - await self.async_set_value(TONE_ID_DEFAULT, options) + await self.async_set_value(ToneID.DEFAULT, options) return tone_id = int( @@ -102,4 +103,4 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - await self.async_set_value(TONE_ID_OFF) + await self.async_set_value(ToneID.OFF) diff --git a/requirements_all.txt b/requirements_all.txt index fa3b4d2ba4c..32223f0fdfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2461,4 +2461,4 @@ zigpy==0.35.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.27.1 +zwave-js-server-python==0.28.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb34d203904..2baf16de687 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1355,4 +1355,4 @@ zigpy-znp==0.5.1 zigpy==0.35.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.27.1 +zwave-js-server-python==0.28.0 diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 92ead72e2ad..4b66e178e6f 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -35,6 +35,7 @@ from .common import ( AEON_SMART_SWITCH_LIGHT_ENTITY, AIR_TEMPERATURE_SENSOR, CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY, ) @@ -805,7 +806,7 @@ async def test_multicast_set_value( hass, client, climate_danfoss_lc_13, - climate_radio_thermostat_ct100_plus_different_endpoints, + climate_eurotronic_spirit_z, integration, ): """Test multicast_set_value service.""" @@ -816,7 +817,7 @@ async def test_multicast_set_value( { ATTR_ENTITY_ID: [ CLIMATE_DANFOSS_LC13_ENTITY, - CLIMATE_RADIO_THERMOSTAT_ENTITY, + CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, ], ATTR_COMMAND_CLASS: 117, ATTR_PROPERTY: "local", @@ -829,7 +830,7 @@ async def test_multicast_set_value( args = client.async_send_command.call_args[0][0] assert args["command"] == "multicast_group.set_value" assert args["nodeIDs"] == [ - climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + climate_eurotronic_spirit_z.node_id, climate_danfoss_lc_13.node_id, ] assert args["valueId"] == { @@ -847,7 +848,7 @@ async def test_multicast_set_value( { ATTR_ENTITY_ID: [ CLIMATE_DANFOSS_LC13_ENTITY, - CLIMATE_RADIO_THERMOSTAT_ENTITY, + CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, ], ATTR_COMMAND_CLASS: 117, ATTR_PROPERTY: "local", @@ -860,7 +861,7 @@ async def test_multicast_set_value( args = client.async_send_command.call_args[0][0] assert args["command"] == "multicast_group.set_value" assert args["nodeIDs"] == [ - climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + climate_eurotronic_spirit_z.node_id, climate_danfoss_lc_13.node_id, ] assert args["valueId"] == { @@ -937,7 +938,7 @@ async def test_multicast_set_value( { ATTR_ENTITY_ID: [ CLIMATE_DANFOSS_LC13_ENTITY, - CLIMATE_RADIO_THERMOSTAT_ENTITY, + CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, ], ATTR_COMMAND_CLASS: 117, ATTR_PROPERTY: "local", diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 23507e6a705..937b2c0fa67 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -53,6 +53,7 @@ TONE_ID_VALUE_ID = { "30": "30DOOR~1 (27 sec)", "255": "default", }, + "valueChangeOptions": ["volume"], }, } diff --git a/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json b/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json index 6bf7ece9758..5616abd6e0f 100644 --- a/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json +++ b/tests/fixtures/zwave_js/aeotec_zw164_siren_state.json @@ -2597,7 +2597,8 @@ "writeable": true, "label": "Tone ID", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["volume"] } }, { @@ -2726,7 +2727,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { @@ -2856,7 +2858,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { @@ -3011,7 +3014,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { @@ -3166,7 +3170,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { @@ -3321,7 +3326,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { @@ -3451,7 +3457,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { @@ -3581,7 +3588,8 @@ "29": "29UPWA~1 (2 sec)", "30": "30DOOR~1 (27 sec)", "255": "default" - } + }, + "valueChangeOptions": ["volume"] } }, { diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json index 58608131e90..dfa72af6aa4 100644 --- a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -267,7 +267,8 @@ "min": 0, "max": 255, "label": "Target value (Warm White)", - "description": "The target value of the Warm White color." + "description": "The target value of the Warm White color.", + "valueChangeOptions": ["transitionDuration"] } }, { @@ -285,7 +286,8 @@ "min": 0, "max": 255, "label": "Target value (Cold White)", - "description": "The target value of the Cold White color." + "description": "The target value of the Cold White color.", + "valueChangeOptions": ["transitionDuration"] } }, { @@ -303,7 +305,8 @@ "min": 0, "max": 255, "label": "Target value (Red)", - "description": "The target value of the Red color." + "description": "The target value of the Red color.", + "valueChangeOptions": ["transitionDuration"] } }, { @@ -321,7 +324,8 @@ "min": 0, "max": 255, "label": "Target value (Green)", - "description": "The target value of the Green color." + "description": "The target value of the Green color.", + "valueChangeOptions": ["transitionDuration"] } }, { @@ -339,7 +343,8 @@ "min": 0, "max": 255, "label": "Target value (Blue)", - "description": "The target value of the Blue color." + "description": "The target value of the Blue color.", + "valueChangeOptions": ["transitionDuration"] } }, { From ce382a39d056d9f3a6210e1d17d03171f8bca627 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Jul 2021 23:37:33 -0700 Subject: [PATCH 497/818] Block title in strings.json unless internal or allowed (#53304) --- homeassistant/components/airnow/strings.json | 1 - .../components/apple_tv/strings.json | 1 - .../components/bosch_shc/strings.json | 3 +- .../components/climacell/strings.json | 1 - homeassistant/components/foscam/strings.json | 1 - .../components/habitica/strings.json | 33 +++++++------ .../components/home_plus_control/strings.json | 1 - .../components/kostal_plenticore/strings.json | 3 +- homeassistant/components/mazda/strings.json | 46 +++++++++---------- .../components/mysensors/strings.json | 1 - homeassistant/components/neato/strings.json | 5 +- homeassistant/components/picnic/strings.json | 3 +- homeassistant/components/sia/strings.json | 1 - .../components/srp_energy/strings.json | 1 - .../components/syncthing/strings.json | 1 - homeassistant/components/wallbox/strings.json | 3 +- .../components/zwave_js/strings.json | 3 +- script/hassfest/model.py | 10 ++++ script/hassfest/translations.py | 28 +++++++++++ 19 files changed, 83 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index a73ad6d179c..9fc5bd3bccc 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -1,5 +1,4 @@ { - "title": "AirNow", "config": { "step": { "user": { diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index 00dd92cac89..d9fe17863dd 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -1,5 +1,4 @@ { - "title": "Apple TV", "config": { "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index e7f090a4e1b..15fb061ef2b 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -1,5 +1,4 @@ { - "title": "Bosch SHC", "config": { "step": { "user": { @@ -35,4 +34,4 @@ }, "flow_title": "Bosch SHC: {name}" } -} \ No newline at end of file +} diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json index f4347d254b7..44021f4b6d0 100644 --- a/homeassistant/components/climacell/strings.json +++ b/homeassistant/components/climacell/strings.json @@ -1,5 +1,4 @@ { - "title": "ClimaCell", "config": { "step": { "user": { diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 5c0622af9d1..14aa88b7952 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -1,5 +1,4 @@ { - "title": "Foscam", "config": { "step": { "user": { diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 868d024b02e..d25b840d761 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -1,20 +1,19 @@ { - "config": { - "error": { - "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "url": "[%key:common::config_flow::data::url%]", - "name": "Override for Habitica’s username. Will be used for service calls", - "api_user": "Habitica’s API user ID", - "api_key": "[%key:common::config_flow::data::api_key%]" - }, - "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" - } - } + "config": { + "error": { + "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, - "title": "Habitica" + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "name": "Override for Habitica’s username. Will be used for service calls", + "api_user": "Habitica’s API user ID", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" + } + } + } } diff --git a/homeassistant/components/home_plus_control/strings.json b/homeassistant/components/home_plus_control/strings.json index c991c9e0279..9e860b397fb 100644 --- a/homeassistant/components/home_plus_control/strings.json +++ b/homeassistant/components/home_plus_control/strings.json @@ -1,5 +1,4 @@ { - "title": "Legrand Home+ Control", "config": { "step": { "pick_implementation": { diff --git a/homeassistant/components/kostal_plenticore/strings.json b/homeassistant/components/kostal_plenticore/strings.json index 771c3ada744..30ce5af5a6c 100644 --- a/homeassistant/components/kostal_plenticore/strings.json +++ b/homeassistant/components/kostal_plenticore/strings.json @@ -1,5 +1,4 @@ { - "title": "Kostal Plenticore Solar Inverter", "config": { "step": { "user": { @@ -18,4 +17,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index a7bed8725af..d2cc1bcfec9 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -1,26 +1,24 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "error": { - "account_locked": "Account locked. Please try again later.", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "Region" - }, - "description": "Please enter the email address and password you use to log into the MyMazda mobile app.", - "title": "Mazda Connected Services - Add Account" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, - "title": "Mazda Connected Services" -} \ No newline at end of file + "error": { + "account_locked": "Account locked. Please try again later.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "region": "Region" + }, + "description": "Please enter the email address and password you use to log into the MyMazda mobile app." + } + } + } +} diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index 54821877b4f..d7722e565cb 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -1,5 +1,4 @@ { - "title": "MySensors", "config": { "step": { "user": { diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index 21af0f91d17..20848ccff08 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -18,6 +18,5 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } - }, - "title": "Neato Botvac" -} \ No newline at end of file + } +} diff --git a/homeassistant/components/picnic/strings.json b/homeassistant/components/picnic/strings.json index d43a91fbb0c..7fbd5e9bef6 100644 --- a/homeassistant/components/picnic/strings.json +++ b/homeassistant/components/picnic/strings.json @@ -1,5 +1,4 @@ { - "title": "Picnic", "config": { "step": { "user": { @@ -19,4 +18,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index f837d41056a..fe648c24e75 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -1,5 +1,4 @@ { - "title": "SIA Alarm Systems", "config": { "step": { "user": { diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json index 8dce61229a9..3dddd961194 100644 --- a/homeassistant/components/srp_energy/strings.json +++ b/homeassistant/components/srp_energy/strings.json @@ -1,5 +1,4 @@ { - "title": "SRP Energy", "config": { "step": { "user": { diff --git a/homeassistant/components/syncthing/strings.json b/homeassistant/components/syncthing/strings.json index 1781df56f1e..36d1a688a70 100644 --- a/homeassistant/components/syncthing/strings.json +++ b/homeassistant/components/syncthing/strings.json @@ -1,5 +1,4 @@ { - "title": "Syncthing", "config": { "step": { "user": { diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json index 63fc5d89e85..6824a1343fc 100644 --- a/homeassistant/components/wallbox/strings.json +++ b/homeassistant/components/wallbox/strings.json @@ -1,5 +1,4 @@ { - "title": "Wallbox", "config": { "step": { "user": { @@ -19,4 +18,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 3d5aa277943..628451a6215 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -1,5 +1,4 @@ { - "title": "Z-Wave JS", "config": { "step": { "manual": { @@ -113,4 +112,4 @@ "value": "Current value of a Z-Wave Value" } } -} \ No newline at end of file +} diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 59d75be5c4a..b20df6ea42f 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -86,6 +86,16 @@ class Integration: """Return if integration is disabled.""" return self.manifest.get("disabled") + @property + def name(self) -> str: + """Return name of the integration.""" + return self.manifest["name"] + + @property + def quality_scale(self) -> str: + """Return quality scale of the integration.""" + return self.manifest.get("quality_scale") + @property def requirements(self) -> list[str]: """List of requirements.""" diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 4143d61ca5d..e24b37d71d9 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -21,6 +21,20 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" +# Only allow translatino of integration names if they contain non-brand names +ALLOW_NAME_TRANSLATION = { + "cert_expiry", + "emulated_roku", + "garages_amsterdam", + "google_travel_time", + "homekit_controller", + "islamic_prayer_times", + "local_ip", + "nmap_tracker", + "rpi_power", + "waze_travel_time", +} + REMOVED_TITLE_MSG = ( "config.title key has been moved out of config and into the root of strings.json. " "Starting Home Assistant 0.109 you only need to define this key in the root " @@ -257,6 +271,20 @@ def validate_translation_file(config: Config, integration: Integration, all_stri if strings_file.name == "strings.json": find_references(strings, name, references) + if ( + integration.domain not in ALLOW_NAME_TRANSLATION + # Only enforce for core because custom integratinos can't be + # added to allow list. + and integration.core + and strings.get("title") == integration.name + and integration.quality_scale != "internal" + ): + integration.add_error( + "translations", + "Don't specify title in translation strings if it's a brand name " + "or add exception to ALLOW_NAME_TRANSLATION", + ) + platform_string_schema = gen_platform_strings_schema(config, integration) platform_strings = [integration.path.glob("strings.*.json")] From 4df928c1888813e539bafd76518b010f9f9a06fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jul 2021 20:38:55 -1000 Subject: [PATCH 498/818] Add support for updating the ISY ip address from discovery (#53290) Co-authored-by: Franck Nijhof --- .../components/isy994/config_flow.py | 64 ++++- homeassistant/components/isy994/const.py | 6 + tests/components/isy994/test_config_flow.py | 232 ++++++++++++++++++ 3 files changed, 292 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index c9ca29e8f63..58e5238cbee 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,6 +1,6 @@ """Config flow for Universal Devices ISY994 integration.""" import logging -from urllib.parse import urlparse +from urllib.parse import urlparse, urlunparse from aiohttp import CookieJar import async_timeout @@ -9,7 +9,7 @@ from pyisy.configuration import Configuration from pyisy.connection import Connection import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, core, data_entry_flow, exceptions from homeassistant.components import ssdp from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -28,7 +28,11 @@ from .const import ( DEFAULT_TLS_VERSION, DEFAULT_VAR_SENSOR_STRING, DOMAIN, + HTTP_PORT, + HTTPS_PORT, ISY_URL_POSTFIX, + SCHEME_HTTP, + SCHEME_HTTPS, UDN_UUID_PREFIX, ) @@ -58,15 +62,15 @@ async def validate_input(hass: core.HomeAssistant, data): host = urlparse(data[CONF_HOST]) tls_version = data.get(CONF_TLS_VER) - if host.scheme == "http": + if host.scheme == SCHEME_HTTP: https = False - port = host.port or 80 + port = host.port or HTTP_PORT session = aiohttp_client.async_create_clientsession( hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) ) - elif host.scheme == "https": + elif host.scheme == SCHEME_HTTPS: https = True - port = host.port or 443 + port = host.port or HTTPS_PORT session = aiohttp_client.async_get_clientsession(hass) else: _LOGGER.error("The isy994 host value in configuration is invalid") @@ -150,6 +154,39 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import.""" return await self.async_step_user(user_input) + async def _async_set_unique_id_or_update(self, isy_mac, ip_address, port) -> None: + """Abort and update the ip address on change.""" + existing_entry = await self.async_set_unique_id(isy_mac) + if not existing_entry: + return + parsed_url = urlparse(existing_entry.data[CONF_HOST]) + if parsed_url.hostname != ip_address: + new_netloc = ip_address + if port: + new_netloc = f"{ip_address}:{port}" + elif parsed_url.port: + new_netloc = f"{ip_address}:{parsed_url.port}" + self.hass.config_entries.async_update_entry( + existing_entry, + data={ + **existing_entry.data, + CONF_HOST: urlunparse( + ( + parsed_url.scheme, + new_netloc, + parsed_url.path, + parsed_url.query, + parsed_url.fragment, + None, + ) + ), + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + raise data_entry_flow.AbortFlow("already_configured") + async def async_step_dhcp(self, discovery_info): """Handle a discovered isy994 via dhcp.""" friendly_name = discovery_info[HOSTNAME] @@ -158,8 +195,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): isy_mac = ( f"{mac[0:2]}:{mac[2:4]}:{mac[4:6]}:{mac[6:8]}:{mac[8:10]}:{mac[10:12]}" ) - await self.async_set_unique_id(isy_mac) - self._abort_if_unique_id_configured() + await self._async_set_unique_id_or_update( + isy_mac, discovery_info[IP_ADDRESS], None + ) self.discovered_conf = { CONF_NAME: friendly_name, @@ -173,14 +211,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a discovered isy994.""" friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] url = discovery_info[ssdp.ATTR_SSDP_LOCATION] + parsed_url = urlparse(url) mac = discovery_info[ssdp.ATTR_UPNP_UDN] if mac.startswith(UDN_UUID_PREFIX): mac = mac[len(UDN_UUID_PREFIX) :] if url.endswith(ISY_URL_POSTFIX): url = url[: -len(ISY_URL_POSTFIX)] - await self.async_set_unique_id(mac) - self._abort_if_unique_id_configured() + port = HTTP_PORT + if parsed_url.port: + port = parsed_url.port + elif parsed_url.scheme == SCHEME_HTTPS: + port = HTTPS_PORT + + await self._async_set_unique_id_or_update(mac, parsed_url.hostname, port) self.discovered_conf = { CONF_NAME: friendly_name, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 343f01332f2..b7b2f283a84 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -672,3 +672,9 @@ BINARY_SENSOR_DEVICE_TYPES_ZWAVE = { DEVICE_CLASS_MOTION: ["155"], DEVICE_CLASS_VIBRATION: ["173"], } + + +SCHEME_HTTP = "http" +HTTP_PORT = 80 +SCHEME_HTTPS = "https" +HTTPS_PORT = 443 diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index e5458a3c96b..1e96de9ff2f 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -156,6 +156,24 @@ async def test_form_invalid_auth(hass: HomeAssistant): assert result2["errors"] == {"base": "invalid_auth"} +async def test_form_unknown_exeption(hass: HomeAssistant): + """Test we handle generic exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + PATCH_CONNECTION, + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + async def test_form_isy_connection_error(hass: HomeAssistant): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( @@ -355,6 +373,146 @@ async def test_form_ssdp(hass: HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_ssdp_existing_entry(hass: HomeAssistant): + """Test we update the ip of an existing entry from ssdp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}"}, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: f"http://3.3.3.3{ISY_URL_POSTFIX}", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == f"http://3.3.3.3:80{ISY_URL_POSTFIX}" + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): + """Test we update the ip of an existing entry from ssdp with no port.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}:1443/{ISY_URL_POSTFIX}"}, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: f"http://3.3.3.3/{ISY_URL_POSTFIX}", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == f"http://3.3.3.3:80/{ISY_URL_POSTFIX}" + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant): + """Test we update the ip of an existing entry from ssdp with an alternate port.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}:1443/{ISY_URL_POSTFIX}"}, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}" + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): + """Test we update the ip of an existing entry from ssdp with no port and https.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"https://{MOCK_HOSTNAME}/{ISY_URL_POSTFIX}"}, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: f"https://3.3.3.3/{ISY_URL_POSTFIX}", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == f"https://3.3.3.3:443/{ISY_URL_POSTFIX}" + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_dhcp(hass: HomeAssistant): """Test we can setup from dhcp.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -390,3 +548,77 @@ async def test_form_dhcp(hass: HomeAssistant): assert result2["data"] == MOCK_USER_INPUT assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_dhcp_existing_entry(hass: HomeAssistant): + """Test we update the ip of an existing entry from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}"}, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data={ + dhcp.IP_ADDRESS: "1.2.3.4", + dhcp.HOSTNAME: "isy994-ems", + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == f"http://1.2.3.4{ISY_URL_POSTFIX}" + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): + """Test we update the ip of an existing entry from dhcp preserves port.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "bob", + CONF_HOST: f"http://{MOCK_HOSTNAME}:1443{ISY_URL_POSTFIX}", + }, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data={ + dhcp.IP_ADDRESS: "1.2.3.4", + dhcp.HOSTNAME: "isy994-ems", + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" + assert entry.data[CONF_USERNAME] == "bob" + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 804499968ee6b5967b45ca7825f054a39fb405c9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 22 Jul 2021 02:51:14 -0400 Subject: [PATCH 499/818] Use entity class attributes for Bluesound (#53033) * Use entity class attributes for bluesound * rework * tweak * tweak --- .../components/bluesound/media_player.py | 185 +++++++----------- 1 file changed, 70 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 86d0be72bdc..a565a0f560c 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -203,33 +203,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" - def __init__(self, hass, host, port=None, name=None, init_callback=None): + _attr_media_content_type = MEDIA_TYPE_MUSIC + + def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None): """Initialize the media player.""" self.host = host self._hass = hass self.port = port self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. - self._name = name - self._icon = None + self._attr_name = name self._capture_items = [] self._services_items = [] self._preset_items = [] self._sync_status = {} self._status = None - self._last_status_update = None - self._is_online = False + self._is_online = None self._retry_remove = None - self._muted = False self._master = None - self._is_master = False self._group_name = None - self._group_list = [] self._bluesound_device_name = None - + self._is_master = False + self._group_list = [] self._init_callback = init_callback - if self.port is None: - self.port = DEFAULT_PORT class _TimeoutException(Exception): pass @@ -252,12 +248,12 @@ class BluesoundPlayer(MediaPlayerEntity): return None self._sync_status = resp["SyncStatus"].copy() - if not self._name: - self._name = self._sync_status.get("@name", self.host) + if not self.name: + self._attr_name = self._sync_status.get("@name", self.host) if not self._bluesound_device_name: self._bluesound_device_name = self._sync_status.get("@name", self.host) - if not self._icon: - self._icon = self._sync_status.get("@icon", self.host) + if not self.icon: + self._attr_icon = self._sync_status.get("@icon", self.host) master = self._sync_status.get("master") if master is not None: @@ -291,14 +287,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): - _LOGGER.info("Node %s is offline, retrying later", self._name) + _LOGGER.info("Node %s is offline, retrying later", self.name) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s", self._name) + _LOGGER.debug("Stopping the polling of node %s", self.name) except Exception: - _LOGGER.exception("Unexpected error in %s", self._name) + _LOGGER.exception("Unexpected error in %s", self.name) raise def start_polling(self): @@ -402,7 +398,7 @@ class BluesoundPlayer(MediaPlayerEntity): if response.status == HTTP_OK: result = await response.text() self._is_online = True - self._last_status_update = dt_util.utcnow() + self._attr_media_position_updated_at = dt_util.utcnow() self._status = xmltodict.parse(result)["status"].copy() group_name = self._status.get("groupName") @@ -438,11 +434,58 @@ class BluesoundPlayer(MediaPlayerEntity): except (asyncio.TimeoutError, ClientError): self._is_online = False - self._last_status_update = None + self._attr_media_position_updated_at = None self._status = None self.async_write_ha_state() - _LOGGER.info("Client connection error, marking %s as offline", self._name) + _LOGGER.info("Client connection error, marking %s as offline", self.name) raise + self.update_state_attr() + + def update_state_attr(self): + """Update state attributes.""" + if self._status is None: + self._attr_state = STATE_OFF + self._attr_supported_features = 0 + elif self.is_grouped and not self.is_master: + self._attr_state = STATE_GROUPED + self._attr_supported_features = ( + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE + ) + else: + status = self._status.get("state") + self._attr_state = STATE_IDLE + if status in ("pause", "stop"): + self._attr_state = STATE_PAUSED + elif status in ("stream", "play"): + self._attr_state = STATE_PLAYING + supported = SUPPORT_CLEAR_PLAYLIST + if self._status.get("indexing", "0") == "0": + supported = ( + supported + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_PLAY + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET + ) + if self.volume_level is not None and self.volume_level >= 0: + supported = ( + supported + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + ) + if self._status.get("canSeek", "") == "1": + supported = supported | SUPPORT_SEEK + self._attr_supported_features = supported + self._attr_extra_state_attributes = {} + if self._group_list: + self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list} + self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master + self._attr_shuffle = self._status.get("shuffle", "0") == "1" async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" @@ -542,27 +585,6 @@ class BluesoundPlayer(MediaPlayerEntity): return self._services_items - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): - """Return the state of the device.""" - if self._status is None: - return STATE_OFF - - if self.is_grouped and not self.is_master: - return STATE_GROUPED - - status = self._status.get("state") - if status in ("pause", "stop"): - return STATE_PAUSED - if status in ("stream", "play"): - return STATE_PLAYING - return STATE_IDLE - @property def media_title(self): """Title of current playing media.""" @@ -617,7 +639,7 @@ class BluesoundPlayer(MediaPlayerEntity): return None mediastate = self.state - if self._last_status_update is None or mediastate == STATE_IDLE: + if self.media_position_updated_at is None or mediastate == STATE_IDLE: return None position = self._status.get("secs") @@ -626,7 +648,9 @@ class BluesoundPlayer(MediaPlayerEntity): position = float(position) if mediastate == STATE_PLAYING: - position += (dt_util.utcnow() - self._last_status_update).total_seconds() + position += ( + dt_util.utcnow() - self.media_position_updated_at + ).total_seconds() return position @@ -641,11 +665,6 @@ class BluesoundPlayer(MediaPlayerEntity): return None return float(duration) - @property - def media_position_updated_at(self): - """Last time status was updated.""" - return self._last_status_update - @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -668,21 +687,11 @@ class BluesoundPlayer(MediaPlayerEntity): mute = bool(int(mute)) return mute - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def bluesound_device_name(self): """Return the device name as returned by the device.""" return self._bluesound_device_name - @property - def icon(self): - """Return the icon of the device.""" - return self._icon - @property def source_list(self): """List of available input sources.""" @@ -778,58 +787,15 @@ class BluesoundPlayer(MediaPlayerEntity): return None @property - def supported_features(self): - """Flag of media commands that are supported.""" - if self._status is None: - return 0 - - if self.is_grouped and not self.is_master: - return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE - - supported = SUPPORT_CLEAR_PLAYLIST - - if self._status.get("indexing", "0") == "0": - supported = ( - supported - | SUPPORT_PAUSE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_PLAY - | SUPPORT_SELECT_SOURCE - | SUPPORT_SHUFFLE_SET - ) - - current_vol = self.volume_level - if current_vol is not None and current_vol >= 0: - supported = ( - supported - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - ) - - if self._status.get("canSeek", "") == "1": - supported = supported | SUPPORT_SEEK - - return supported - - @property - def is_master(self): + def is_master(self) -> bool: """Return true if player is a coordinator.""" return self._is_master @property - def is_grouped(self): + def is_grouped(self) -> bool: """Return true if player is a coordinator.""" return self._master is not None or self._is_master - @property - def shuffle(self): - """Return true if shuffle is active.""" - return self._status.get("shuffle", "0") == "1" - async def async_join(self, master): """Join the player to a group.""" master_device = [ @@ -849,17 +815,6 @@ class BluesoundPlayer(MediaPlayerEntity): else: _LOGGER.error("Master not found %s", master_device) - @property - def extra_state_attributes(self): - """List members in group.""" - attributes = {} - if self._group_list: - attributes = {ATTR_BLUESOUND_GROUP: self._group_list} - - attributes[ATTR_MASTER] = self._is_master - - return attributes - def rebuild_bluesound_group(self): """Rebuild the list of entities in speaker group.""" if self._group_name is None: From 9753500f5eaff7354b218a405d34a43edc904c4e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 22 Jul 2021 08:57:29 +0200 Subject: [PATCH 500/818] Disable speeds for first gen Xiaomi_miio air purifiers (#52772) * Disable speeds for first gen air purifiers * Remove test code line * remove OPERATION_MODES_AIRPURIFIER list --- homeassistant/components/xiaomi_miio/fan.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c1dde7ec38d..bdef9517cca 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -336,7 +336,6 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = { ATTR_EXTRA_FEATURES: "extra_features", } -OPERATION_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"] PRESET_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"] OPERATION_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] PRESET_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] @@ -903,10 +902,10 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self._device_features = FEATURE_FLAGS_AIRPURIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER self._preset_modes = PRESET_MODES_AIRPURIFIER - self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE - self._speed_count = 4 + self._supported_features = SUPPORT_PRESET_MODE + self._speed_count = 1 # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER + self._speed_list = [] self._state_attrs.update( {attribute: None for attribute in self._available_attributes} From 7768f53281a6d294a55445a52efd967d8e30098b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 10:36:29 +0200 Subject: [PATCH 501/818] Use NamedTuple - brother (#53330) --- homeassistant/components/brother/const.py | 352 ++++++++++----------- homeassistant/components/brother/model.py | 12 +- homeassistant/components/brother/sensor.py | 27 +- 3 files changed, 190 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index c0021df11fc..727a67d9093 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -3,15 +3,10 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - DEVICE_CLASS_TIMESTAMP, - PERCENTAGE, -) +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE -from .model import SensorDescription +from .model import BrotherSensorMetadata ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" @@ -31,9 +26,7 @@ ATTR_DRUM_COUNTER: Final = "drum_counter" ATTR_DRUM_REMAINING_LIFE: Final = "drum_remaining_life" ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages" ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter" -ATTR_ENABLED: Final = "enabled" ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life" -ATTR_LABEL: Final = "label" ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life" ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter" ATTR_MAGENTA_DRUM_REMAINING_LIFE: Final = "magenta_drum_remaining_life" @@ -46,7 +39,6 @@ ATTR_PF_KIT_1_REMAINING_LIFE: Final = "pf_kit_1_remaining_life" ATTR_PF_KIT_MP_REMAINING_LIFE: Final = "pf_kit_mp_remaining_life" ATTR_REMAINING_PAGES: Final = "remaining_pages" ATTR_STATUS: Final = "status" -ATTR_UNIT: Final = "unit" ATTR_UPTIME: Final = "uptime" ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter" ATTR_YELLOW_DRUM_REMAINING_LIFE: Final = "yellow_drum_remaining_life" @@ -84,174 +76,172 @@ ATTRS_MAP: Final[dict[str, tuple[str, str]]] = { ), } -SENSOR_TYPES: Final[dict[str, SensorDescription]] = { - ATTR_STATUS: { - ATTR_ICON: "mdi:printer", - ATTR_LABEL: ATTR_STATUS.title(), - ATTR_UNIT: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: None, - }, - ATTR_PAGE_COUNTER: { - ATTR_ICON: "mdi:file-document-outline", - ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(), - ATTR_UNIT: UNIT_PAGES, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BW_COUNTER: { - ATTR_ICON: "mdi:file-document-outline", - ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(), - ATTR_UNIT: UNIT_PAGES, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_COLOR_COUNTER: { - ATTR_ICON: "mdi:file-document-outline", - ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(), - ATTR_UNIT: UNIT_PAGES, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_DUPLEX_COUNTER: { - ATTR_ICON: "mdi:file-document-outline", - ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(), - ATTR_UNIT: UNIT_PAGES, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_DRUM_REMAINING_LIFE: { - ATTR_ICON: "mdi:chart-donut", - ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BLACK_DRUM_REMAINING_LIFE: { - ATTR_ICON: "mdi:chart-donut", - ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_CYAN_DRUM_REMAINING_LIFE: { - ATTR_ICON: "mdi:chart-donut", - ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_MAGENTA_DRUM_REMAINING_LIFE: { - ATTR_ICON: "mdi:chart-donut", - ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_YELLOW_DRUM_REMAINING_LIFE: { - ATTR_ICON: "mdi:chart-donut", - ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BELT_UNIT_REMAINING_LIFE: { - ATTR_ICON: "mdi:current-ac", - ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_FUSER_REMAINING_LIFE: { - ATTR_ICON: "mdi:water-outline", - ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_LASER_REMAINING_LIFE: { - ATTR_ICON: "mdi:spotlight-beam", - ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_PF_KIT_1_REMAINING_LIFE: { - ATTR_ICON: "mdi:printer-3d", - ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_PF_KIT_MP_REMAINING_LIFE: { - ATTR_ICON: "mdi:printer-3d", - ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BLACK_TONER_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_CYAN_TONER_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_MAGENTA_TONER_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_YELLOW_TONER_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BLACK_INK_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_CYAN_INK_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_MAGENTA_INK_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_YELLOW_INK_REMAINING: { - ATTR_ICON: "mdi:printer-3d-nozzle", - ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: PERCENTAGE, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_UPTIME: { - ATTR_ICON: None, - ATTR_LABEL: ATTR_UPTIME.title(), - ATTR_UNIT: None, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, - }, +SENSOR_TYPES: Final[dict[str, BrotherSensorMetadata]] = { + ATTR_STATUS: BrotherSensorMetadata( + icon="mdi:printer", + label=ATTR_STATUS.title(), + unit_of_measurement=None, + enabled=True, + ), + ATTR_PAGE_COUNTER: BrotherSensorMetadata( + icon="mdi:file-document-outline", + label=ATTR_PAGE_COUNTER.replace("_", " ").title(), + unit_of_measurement=UNIT_PAGES, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_BW_COUNTER: BrotherSensorMetadata( + icon="mdi:file-document-outline", + label=ATTR_BW_COUNTER.replace("_", " ").title(), + unit_of_measurement=UNIT_PAGES, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_COLOR_COUNTER: BrotherSensorMetadata( + icon="mdi:file-document-outline", + label=ATTR_COLOR_COUNTER.replace("_", " ").title(), + unit_of_measurement=UNIT_PAGES, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_DUPLEX_COUNTER: BrotherSensorMetadata( + icon="mdi:file-document-outline", + label=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + unit_of_measurement=UNIT_PAGES, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:chart-donut", + label=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_BLACK_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:chart-donut", + label=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_CYAN_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:chart-donut", + label=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_MAGENTA_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:chart-donut", + label=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_YELLOW_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:chart-donut", + label=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_BELT_UNIT_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:current-ac", + label=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_FUSER_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:water-outline", + label=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_LASER_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:spotlight-beam", + label=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_PF_KIT_1_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:printer-3d", + label=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_PF_KIT_MP_REMAINING_LIFE: BrotherSensorMetadata( + icon="mdi:printer-3d", + label=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_BLACK_TONER_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_CYAN_TONER_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_MAGENTA_TONER_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_YELLOW_TONER_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_BLACK_INK_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_CYAN_INK_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_MAGENTA_INK_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_YELLOW_INK_REMAINING: BrotherSensorMetadata( + icon="mdi:printer-3d-nozzle", + label=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + unit_of_measurement=PERCENTAGE, + enabled=True, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_UPTIME: BrotherSensorMetadata( + icon=None, + label=ATTR_UPTIME.title(), + unit_of_measurement=None, + enabled=False, + device_class=DEVICE_CLASS_TIMESTAMP, + ), } diff --git a/homeassistant/components/brother/model.py b/homeassistant/components/brother/model.py index ab8df09b749..a1fcc83aae9 100644 --- a/homeassistant/components/brother/model.py +++ b/homeassistant/components/brother/model.py @@ -1,15 +1,15 @@ """Type definitions for Brother integration.""" from __future__ import annotations -from typing import TypedDict +from typing import NamedTuple -class SensorDescription(TypedDict, total=False): - """Sensor description class.""" +class BrotherSensorMetadata(NamedTuple): + """Metadata for an individual Brother sensor.""" icon: str | None label: str - unit: str | None + unit_of_measurement: str | None enabled: bool - state_class: str | None - device_class: str | None + state_class: str | None = None + device_class: str | None = None diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 38fac529076..90a73f1bd9b 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -3,9 +3,8 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,17 +13,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BrotherDataUpdateCoordinator from .const import ( ATTR_COUNTER, - ATTR_ENABLED, - ATTR_LABEL, ATTR_MANUFACTURER, ATTR_REMAINING_PAGES, - ATTR_UNIT, ATTR_UPTIME, ATTRS_MAP, DATA_CONFIG_ENTRY, DOMAIN, SENSOR_TYPES, ) +from .model import BrotherSensorMetadata async def async_setup_entry( @@ -43,9 +40,11 @@ async def async_setup_entry( "sw_version": getattr(coordinator.data, "firmware", None), } - for sensor in SENSOR_TYPES: + for sensor, metadata in SENSOR_TYPES.items(): if sensor in coordinator.data: - sensors.append(BrotherPrinterSensor(coordinator, sensor, device_info)) + sensors.append( + BrotherPrinterSensor(coordinator, sensor, metadata, device_info) + ) async_add_entities(sensors, False) @@ -56,20 +55,20 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): self, coordinator: BrotherDataUpdateCoordinator, kind: str, + metadata: BrotherSensorMetadata, device_info: DeviceInfo, ) -> None: """Initialize.""" super().__init__(coordinator) - description = SENSOR_TYPES[kind] self._attrs: dict[str, Any] = {} - self._attr_device_class = description.get(ATTR_DEVICE_CLASS) + self._attr_device_class = metadata.device_class self._attr_device_info = device_info - self._attr_entity_registry_enabled_default = description[ATTR_ENABLED] - self._attr_icon = description[ATTR_ICON] - self._attr_name = f"{coordinator.data.model} {description[ATTR_LABEL]}" - self._attr_state_class = description[ATTR_STATE_CLASS] + self._attr_entity_registry_enabled_default = metadata.enabled + self._attr_icon = metadata.icon + self._attr_name = f"{coordinator.data.model} {metadata.label}" + self._attr_state_class = metadata.state_class self._attr_unique_id = f"{coordinator.data.serial.lower()}_{kind}" - self._attr_unit_of_measurement = description[ATTR_UNIT] + self._attr_unit_of_measurement = metadata.unit_of_measurement self.kind = kind @property From 1a450c208445c6022462c0674c60da64b8a95c68 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 22 Jul 2021 13:25:54 +0300 Subject: [PATCH 502/818] Speedtestdotnet code cleanup and type hints (#52533) --- .coveragerc | 1 - .../components/speedtestdotnet/__init__.py | 104 ++++++------- .../components/speedtestdotnet/config_flow.py | 23 ++- .../components/speedtestdotnet/const.py | 37 +++-- .../components/speedtestdotnet/sensor.py | 50 +++--- .../components/speedtestdotnet/strings.json | 5 +- .../speedtestdotnet/translations/en.json | 3 +- tests/components/speedtestdotnet/conftest.py | 16 ++ .../speedtestdotnet/test_config_flow.py | 146 +++++++++--------- tests/components/speedtestdotnet/test_init.py | 122 +++++++++------ .../components/speedtestdotnet/test_sensor.py | 17 +- 11 files changed, 287 insertions(+), 237 deletions(-) create mode 100644 tests/components/speedtestdotnet/conftest.py diff --git a/.coveragerc b/.coveragerc index 44bb49e5f57..2693080986b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -969,7 +969,6 @@ omit = homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* - homeassistant/components/speedtestdotnet/* homeassistant/components/spider/* homeassistant/components/splunk/* homeassistant/components/spotify/__init__.py diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 71e51c0959d..b049b3a2d2c 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -1,19 +1,22 @@ """Support for testing internet speed via Speedtest.net.""" +from __future__ import annotations + from datetime import timedelta import logging import speedtest import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED, ) -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -22,6 +25,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_SERVER, DOMAIN, + PLATFORMS, SENSOR_TYPES, SPEED_TEST_SERVICE, ) @@ -51,10 +55,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["sensor"] - -def server_id_valid(server_id): +def server_id_valid(server_id: str) -> bool: """Check if server_id is valid.""" try: api = speedtest.Speedtest() @@ -65,7 +67,7 @@ def server_id_valid(server_id): return True -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Import integration from config.""" if DOMAIN in config: hass.async_create_task( @@ -76,7 +78,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Speedtest.net component.""" coordinator = SpeedTestDataCoordinator(hass, config_entry) await coordinator.async_setup() @@ -88,11 +90,9 @@ async def async_setup_entry(hass, config_entry): ) await coordinator.async_refresh() - if not config_entry.options[CONF_MANUAL]: + if not config_entry.options.get(CONF_MANUAL, False): if hass.state == CoreState.running: await _enable_scheduled_speedtests() - if not coordinator.last_update_success: - raise ConfigEntryNotReady else: # Running a speed test during startup can prevent # integrations from being able to setup because it @@ -108,12 +108,10 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload SpeedTest Entry from config_entry.""" hass.services.async_remove(DOMAIN, SPEED_TEST_SERVICE) - hass.data[DOMAIN].async_unload() - unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS ) @@ -125,13 +123,12 @@ async def async_unload_entry(hass, config_entry): class SpeedTestDataCoordinator(DataUpdateCoordinator): """Get the latest data from speedtest.net.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the data object.""" self.hass = hass - self.config_entry = config_entry - self.api = None - self.servers = {} - self._unsub_update_listener = None + self.config_entry: ConfigEntry = config_entry + self.api: speedtest.Speedtest | None = None + self.servers: dict[str, dict] = {DEFAULT_SERVER: {}} super().__init__( self.hass, _LOGGER, @@ -141,51 +138,49 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): def update_servers(self): """Update list of test servers.""" - try: - server_list = self.api.get_servers() - except speedtest.ConfigRetrievalError: - _LOGGER.debug("Error retrieving server list") - return - - self.servers[DEFAULT_SERVER] = {} - for server in sorted( - server_list.values(), - key=lambda server: server[0]["country"] + server[0]["sponsor"], - ): - self.servers[ - f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}" - ] = server[0] + test_servers = self.api.get_servers() + test_servers_list = [] + for servers in test_servers.values(): + for server in servers: + test_servers_list.append(server) + if test_servers_list: + for server in sorted( + test_servers_list, + key=lambda server: ( + server["country"], + server["name"], + server["sponsor"], + ), + ): + self.servers[ + f"{server['country']} - {server['sponsor']} - {server['name']}" + ] = server def update_data(self): """Get the latest data from speedtest.net.""" self.update_servers() - self.api.closest.clear() if self.config_entry.options.get(CONF_SERVER_ID): server_id = self.config_entry.options.get(CONF_SERVER_ID) self.api.get_servers(servers=[server_id]) - try: - self.api.get_best_server() - except speedtest.SpeedtestBestServerFailure as err: - raise UpdateFailed( - "Failed to retrieve best server for speedtest", err - ) from err - + best_server = self.api.get_best_server() _LOGGER.debug( "Executing speedtest.net speed test with server_id: %s", - self.api.best["id"], + best_server["id"], ) self.api.download() self.api.upload() return self.api.results.dict() - async def async_update(self, *_): + async def async_update(self) -> dict[str, str]: """Update Speedtest data.""" try: return await self.hass.async_add_executor_job(self.update_data) - except (speedtest.ConfigRetrievalError, speedtest.NoMatchedServers) as err: - raise UpdateFailed from err + except speedtest.NoMatchedServers as err: + raise UpdateFailed("Selected server is not found.") from err + except speedtest.SpeedtestException as err: + raise UpdateFailed(err) from err async def async_set_options(self): """Set options for entry.""" @@ -200,11 +195,12 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): self.config_entry, data=data, options=options ) - async def async_setup(self): + async def async_setup(self) -> None: """Set up SpeedTest.""" try: self.api = await self.hass.async_add_executor_job(speedtest.Speedtest) - except speedtest.ConfigRetrievalError as err: + await self.hass.async_add_executor_job(self.update_servers) + except speedtest.SpeedtestException as err: raise ConfigEntryNotReady from err async def request_update(call): @@ -213,24 +209,14 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): await self.async_set_options() - await self.hass.async_add_executor_job(self.update_servers) - self.hass.services.async_register(DOMAIN, SPEED_TEST_SERVICE, request_update) - self._unsub_update_listener = self.config_entry.add_update_listener( - options_updated_listener + self.config_entry.async_on_unload( + self.config_entry.add_update_listener(options_updated_listener) ) - @callback - def async_unload(self): - """Unload the coordinator.""" - if not self._unsub_update_listener: - return - self._unsub_update_listener() - self._unsub_update_listener = None - -async def options_updated_listener(hass, entry): +async def options_updated_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" if entry.options[CONF_MANUAL]: hass.data[DOMAIN].update_interval = None diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 49654b6c02b..e5462aa9379 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -1,9 +1,14 @@ """Config flow for Speedtest.net.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import server_id_valid from .const import ( @@ -24,11 +29,15 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: """Get the options flow for this handler.""" return SpeedTestOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -59,14 +68,16 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class SpeedTestOptionsFlowHandler(config_entries.OptionsFlow): """Handle SpeedTest options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - self._servers = {} + self._servers: dict = {} - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: server_name = user_input[CONF_SERVER_NAME] diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 546c7db053b..04f3ea0cc55 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,32 +1,35 @@ """Consts used by Speedtest.net.""" +from typing import Final + from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND, TIME_MILLISECONDS -DOMAIN = "speedtestdotnet" +DOMAIN: Final = "speedtestdotnet" -SPEED_TEST_SERVICE = "speedtest" -DATA_UPDATED = f"{DOMAIN}_data_updated" +SPEED_TEST_SERVICE: Final = "speedtest" -SENSOR_TYPES = { +SENSOR_TYPES: Final = { "ping": ["Ping", TIME_MILLISECONDS], "download": ["Download", DATA_RATE_MEGABITS_PER_SECOND], "upload": ["Upload", DATA_RATE_MEGABITS_PER_SECOND], } -CONF_SERVER_NAME = "server_name" -CONF_SERVER_ID = "server_id" -CONF_MANUAL = "manual" +CONF_SERVER_NAME: Final = "server_name" +CONF_SERVER_ID: Final = "server_id" +CONF_MANUAL: Final = "manual" -ATTR_BYTES_RECEIVED = "bytes_received" -ATTR_BYTES_SENT = "bytes_sent" -ATTR_SERVER_COUNTRY = "server_country" -ATTR_SERVER_ID = "server_id" -ATTR_SERVER_NAME = "server_name" +ATTR_BYTES_RECEIVED: Final = "bytes_received" +ATTR_BYTES_SENT: Final = "bytes_sent" +ATTR_SERVER_COUNTRY: Final = "server_country" +ATTR_SERVER_ID: Final = "server_id" +ATTR_SERVER_NAME: Final = "server_name" -DEFAULT_NAME = "SpeedTest" -DEFAULT_SCAN_INTERVAL = 60 -DEFAULT_SERVER = "*Auto Detect" +DEFAULT_NAME: Final = "SpeedTest" +DEFAULT_SCAN_INTERVAL: Final = 60 +DEFAULT_SERVER: Final = "*Auto Detect" -ATTRIBUTION = "Data retrieved from Speedtest.net by Ookla" +ATTRIBUTION: Final = "Data retrieved from Speedtest.net by Ookla" -ICON = "mdi:speedometer" +ICON: Final = "mdi:speedometer" + +PLATFORMS: Final = ["sensor"] diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index e28aa0b2527..8dcc5bc3459 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -54,26 +54,28 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self._attr_name = f"{DEFAULT_NAME} {SENSOR_TYPES[sensor_type][0]}" self._attr_unit_of_measurement = SENSOR_TYPES[self.type][1] self._attr_unique_id = sensor_type + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def extra_state_attributes(self) -> dict[str, Any] | None: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if not self.coordinator.data: - return None + if self.coordinator.data: + self._attrs.update( + { + ATTR_SERVER_NAME: self.coordinator.data["server"]["name"], + ATTR_SERVER_COUNTRY: self.coordinator.data["server"]["country"], + ATTR_SERVER_ID: self.coordinator.data["server"]["id"], + } + ) - attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_SERVER_NAME: self.coordinator.data["server"]["name"], - ATTR_SERVER_COUNTRY: self.coordinator.data["server"]["country"], - ATTR_SERVER_ID: self.coordinator.data["server"]["id"], - } + if self.type == "download": + self._attrs[ATTR_BYTES_RECEIVED] = self.coordinator.data[ + "bytes_received" + ] + elif self.type == "upload": + self._attrs[ATTR_BYTES_SENT] = self.coordinator.data["bytes_sent"] - if self.type == "download": - attributes[ATTR_BYTES_RECEIVED] = self.coordinator.data["bytes_received"] - elif self.type == "upload": - attributes[ATTR_BYTES_SENT] = self.coordinator.data["bytes_sent"] - - return attributes + return self._attrs async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -91,14 +93,12 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self.async_on_remove(self.coordinator.async_add_listener(update)) self._update_state() - def _update_state(self) -> None: + def _update_state(self): """Update sensors state.""" - if not self.coordinator.data: - return - - if self.type == "ping": - self._attr_state = self.coordinator.data["ping"] - elif self.type == "download": - self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2) - elif self.type == "upload": - self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2) + if self.coordinator.data: + if self.type == "ping": + self._attr_state = self.coordinator.data["ping"] + elif self.type == "download": + self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2) + elif self.type == "upload": + self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2) diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json index cf3587af6c5..c4dad30cb09 100644 --- a/homeassistant/components/speedtestdotnet/strings.json +++ b/homeassistant/components/speedtestdotnet/strings.json @@ -6,8 +6,7 @@ } }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "wrong_server_id": "Server ID is not valid" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "options": { @@ -21,4 +20,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index b56ff193e33..eab480073bc 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible.", - "wrong_server_id": "Server ID is not valid" + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "step": { "user": { diff --git a/tests/components/speedtestdotnet/conftest.py b/tests/components/speedtestdotnet/conftest.py new file mode 100644 index 00000000000..78a864cb934 --- /dev/null +++ b/tests/components/speedtestdotnet/conftest.py @@ -0,0 +1,16 @@ +"""Conftest for speedtestdotnet.""" +from unittest.mock import patch + +import pytest + +from tests.components.speedtestdotnet import MOCK_RESULTS, MOCK_SERVERS + + +@pytest.fixture(autouse=True) +def mock_api(): + """Mock entry setup.""" + with patch("speedtest.Speedtest") as mock_api: + mock_api.return_value.get_servers.return_value = MOCK_SERVERS + mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0] + mock_api.return_value.results.dict.return_value = MOCK_RESULTS + yield mock_api diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index a7a65511ee5..727a5778603 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -1,8 +1,7 @@ """Tests for SpeedTest config flow.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock -import pytest from speedtest import NoMatchedServers from homeassistant import config_entries, data_entry_flow @@ -15,23 +14,12 @@ from homeassistant.components.speedtestdotnet.const import ( SENSOR_TYPES, ) from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL - -from . import MOCK_SERVERS +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -@pytest.fixture(name="mock_setup") -def mock_setup(): - """Mock entry setup.""" - with patch( - "homeassistant.components.speedtestdotnet.async_setup_entry", - return_value=True, - ): - yield - - -async def test_flow_works(hass, mock_setup): +async def test_flow_works(hass: HomeAssistant) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -43,92 +31,104 @@ async def test_flow_works(hass, mock_setup): result["flow_id"], user_input={} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "SpeedTest" -async def test_import_fails(hass, mock_setup): +async def test_import_fails(hass: HomeAssistant, mock_api: MagicMock) -> None: """Test import step fails if server_id is not valid.""" - with patch("speedtest.Speedtest") as mock_api: - mock_api.return_value.get_servers.side_effect = NoMatchedServers - result = await hass.config_entries.flow.async_init( - speedtestdotnet.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_SERVER_ID: "223", - CONF_MANUAL: True, - CONF_SCAN_INTERVAL: timedelta(minutes=1), - CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES), - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "wrong_server_id" + mock_api.return_value.get_servers.side_effect = NoMatchedServers + result = await hass.config_entries.flow.async_init( + speedtestdotnet.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_SERVER_ID: "223", + CONF_MANUAL: True, + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES), + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "wrong_server_id" -async def test_import_success(hass, mock_setup): +async def test_import_success(hass): """Test import step is successful if server_id is valid.""" - with patch("speedtest.Speedtest"): - result = await hass.config_entries.flow.async_init( - speedtestdotnet.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_SERVER_ID: "1", - CONF_MANUAL: True, - CONF_SCAN_INTERVAL: timedelta(minutes=1), - CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES), - }, - ) + result = await hass.config_entries.flow.async_init( + speedtestdotnet.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_SERVER_ID: "1", + CONF_MANUAL: True, + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES), + }, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "SpeedTest" - assert result["data"][CONF_SERVER_ID] == "1" - assert result["data"][CONF_MANUAL] is True - assert result["data"][CONF_SCAN_INTERVAL] == 1 + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "SpeedTest" + assert result["data"][CONF_SERVER_ID] == "1" + assert result["data"][CONF_MANUAL] is True + assert result["data"][CONF_SCAN_INTERVAL] == 1 -async def test_options(hass): +async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: """Test updating options.""" entry = MockConfigEntry( domain=DOMAIN, title="SpeedTest", - data={}, - options={}, ) entry.add_to_hass(hass) - with patch("speedtest.Speedtest") as mock_api: - mock_api.return_value.get_servers.return_value = MOCK_SERVERS - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", - CONF_SCAN_INTERVAL: 30, - CONF_MANUAL: False, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", + CONF_SCAN_INTERVAL: 30, + CONF_MANUAL: True, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", + CONF_SERVER_ID: "1", + CONF_SCAN_INTERVAL: 30, + CONF_MANUAL: True, + } + await hass.async_block_till_done() + + assert hass.data[DOMAIN].update_interval is None + + # test setting the option to update periodically + result2 = await hass.config_entries.options.async_init(entry.entry_id) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={ CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", - CONF_SERVER_ID: "1", CONF_SCAN_INTERVAL: 30, CONF_MANUAL: False, - } + }, + ) + await hass.async_block_till_done() + + assert hass.data[DOMAIN].update_interval == timedelta(minutes=30) -async def test_integration_already_configured(hass): +async def test_integration_already_configured(hass: HomeAssistant) -> None: """Test integration is already configured.""" entry = MockConfigEntry( domain=DOMAIN, - data={}, - options={}, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 30d3d2a1d63..fcadb0e9931 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -1,79 +1,113 @@ """Tests for SpeedTest integration.""" -from unittest.mock import patch +from unittest.mock import MagicMock import speedtest -from homeassistant import config_entries -from homeassistant.components import speedtestdotnet -from homeassistant.setup import async_setup_component +from homeassistant.components.speedtestdotnet.const import ( + CONF_MANUAL, + CONF_SERVER_ID, + CONF_SERVER_NAME, + DOMAIN, + SPEED_TEST_SERVICE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_SCAN_INTERVAL, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_setup_with_config(hass): - """Test that we import the config and setup the integration.""" - config = { - speedtestdotnet.DOMAIN: { - speedtestdotnet.CONF_SERVER_ID: "1", - speedtestdotnet.CONF_MANUAL: True, - speedtestdotnet.CONF_SCAN_INTERVAL: "00:01:00", - } - } - with patch("speedtest.Speedtest"): - assert await async_setup_component(hass, speedtestdotnet.DOMAIN, config) - - -async def test_successful_config_entry(hass): +async def test_successful_config_entry(hass: HomeAssistant) -> None: """Test that SpeedTestDotNet is configured successfully.""" entry = MockConfigEntry( - domain=speedtestdotnet.DOMAIN, + domain=DOMAIN, data={}, + options={ + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", + CONF_SERVER_ID: "1", + CONF_SCAN_INTERVAL: 30, + CONF_MANUAL: False, + }, ) entry.add_to_hass(hass) - with patch("speedtest.Speedtest"), patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", - return_value=True, - ) as forward_entry_setup: - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is config_entries.ConfigEntryState.LOADED - assert forward_entry_setup.mock_calls[0][1] == ( - entry, - "sensor", - ) + assert entry.state == ConfigEntryState.LOADED + assert hass.data[DOMAIN] + assert hass.services.has_service(DOMAIN, SPEED_TEST_SERVICE) -async def test_setup_failed(hass): +async def test_setup_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: """Test SpeedTestDotNet failed due to an error.""" entry = MockConfigEntry( - domain=speedtestdotnet.DOMAIN, - data={}, + domain=DOMAIN, ) entry.add_to_hass(hass) - with patch("speedtest.Speedtest", side_effect=speedtest.ConfigRetrievalError): - - await hass.config_entries.async_setup(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + mock_api.side_effect = speedtest.ConfigRetrievalError + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass): +async def test_unload_entry(hass: HomeAssistant) -> None: """Test removing SpeedTestDotNet.""" entry = MockConfigEntry( - domain=speedtestdotnet.DOMAIN, - data={}, + domain=DOMAIN, ) entry.add_to_hass(hass) - with patch("speedtest.Speedtest"): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() 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 - assert speedtestdotnet.DOMAIN not in hass.data + assert entry.state is ConfigEntryState.NOT_LOADED + assert DOMAIN not in hass.data + + +async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test configured server id is not found.""" + + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + mock_api.return_value.get_servers.side_effect = speedtest.NoMatchedServers + await hass.data[DOMAIN].async_refresh() + await hass.async_block_till_done() + state = hass.states.get("sensor.speedtest_ping") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_get_best_server_error(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test configured server id is not found.""" + + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + mock_api.return_value.get_best_server.side_effect = ( + speedtest.SpeedtestBestServerFailure( + "Unable to connect to servers to test latency." + ) + ) + await hass.data[DOMAIN].async_refresh() + await hass.async_block_till_done() + state = hass.states.get("sensor.speedtest_ping") + assert state is not None + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py index c08a9f3304f..11db05d2994 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -1,26 +1,28 @@ """Tests for SpeedTest sensors.""" -from unittest.mock import patch +from unittest.mock import MagicMock from homeassistant.components import speedtestdotnet from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME, SENSOR_TYPES +from homeassistant.core import HomeAssistant from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES from tests.common import MockConfigEntry -async def test_speedtestdotnet_sensors(hass): +async def test_speedtestdotnet_sensors( + hass: HomeAssistant, mock_api: MagicMock +) -> None: """Test sensors created for speedtestdotnet integration.""" entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={}) entry.add_to_hass(hass) - with patch("speedtest.Speedtest") as mock_api: - mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0] - mock_api.return_value.results.dict.return_value = MOCK_RESULTS + mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0] + mock_api.return_value.results.dict.return_value = MOCK_RESULTS - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 @@ -28,4 +30,5 @@ async def test_speedtestdotnet_sensors(hass): sensor = hass.states.get( f"sensor.{DEFAULT_NAME}_{SENSOR_TYPES[sensor_type][0]}" ) + assert sensor assert sensor.state == MOCK_STATES[sensor_type] From 80c535f02eb322e410f44b24917553d45499c767 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 12:26:50 +0200 Subject: [PATCH 503/818] Use NamedTuple - rova (#53292) Co-authored-by: Franck Nijhof --- homeassistant/components/rova/sensor.py | 48 ++++++++++++++++++------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 13f8fffb8d1..40dab258954 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -1,7 +1,9 @@ """Support for Rova garbage calendar.""" +from __future__ import annotations from datetime import datetime, timedelta import logging +from typing import NamedTuple from requests.exceptions import ConnectTimeout, HTTPError from rova.rova import Rova @@ -24,13 +26,36 @@ CONF_HOUSE_NUMBER_SUFFIX = "house_number_suffix" UPDATE_DELAY = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12) -# Supported sensor types: -# Key: [json_key, name, icon] -SENSOR_TYPES = { - "bio": ["gft", "Biowaste", "mdi:recycle"], - "paper": ["papier", "Paper", "mdi:recycle"], - "plastic": ["pmd", "PET", "mdi:recycle"], - "residual": ["restafval", "Residual", "mdi:recycle"], + +class RovaSensorMetadata(NamedTuple): + """Metadata for an individual rova sensor.""" + + name: str + json_key: str + icon: str + + +SENSOR_TYPES: dict[str, RovaSensorMetadata] = { + "bio": RovaSensorMetadata( + "Biowaste", + json_key="gft", + icon="mdi:recycle", + ), + "paper": RovaSensorMetadata( + "Paper", + json_key="papier", + icon="mdi:recycle", + ), + "plastic": RovaSensorMetadata( + "PET", + json_key="pmd", + icon="mdi:recycle", + ), + "residual": RovaSensorMetadata( + "Residual", + json_key="restafval", + icon="mdi:recycle", + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -90,18 +115,15 @@ class RovaSensor(SensorEntity): self._state = None - self._json_key = SENSOR_TYPES[self.sensor_key][0] + metadata = SENSOR_TYPES[sensor_key] + self._json_key = metadata.json_key + self._attr_icon = metadata.icon @property def name(self): """Return the name.""" return f"{self.platform_name}_{self.sensor_key}" - @property - def icon(self): - """Return the sensor icon.""" - return SENSOR_TYPES[self.sensor_key][2] - @property def device_class(self): """Return the class of this sensor.""" From 009f34bfed0378b378c0a9f8b60112fd6ca4438c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Jul 2021 00:44:36 -1000 Subject: [PATCH 504/818] Add a homekit.unpair service to forcefully remove pairings (#53303) - Sometimes homekit will go unresponsive because a pairing for a specific device is missing. To avoid deleting the config entry and recreating it, which can be a painful process if there are many bridged entities, the homekit.unpair service allows forceful removal of the pairings so the accessory can be paired again. --- homeassistant/components/homekit/__init__.py | 65 ++++++++- homeassistant/components/homekit/const.py | 1 + .../components/homekit/services.yaml | 6 + tests/components/homekit/test_homekit.py | 124 +++++++++++++++++- 4 files changed, 193 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5d9f2037610..a1203b25478 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -23,6 +23,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_IP_ADDRESS, CONF_NAME, @@ -34,11 +35,12 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.reload import async_integration_yaml_config +from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.loader import IntegrationNotFound, async_get_integration from . import ( # noqa: F401 @@ -93,6 +95,7 @@ from .const import ( MANUFACTURER, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, + SERVICE_HOMEKIT_UNPAIR, SHUTDOWN_TIMEOUT, ) from .util import ( @@ -170,6 +173,12 @@ RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema( ) +UNPAIR_SERVICE_SCHEMA = vol.All( + vol.Schema(cv.ENTITY_SERVICE_FIELDS), + cv.has_at_least_one_key(ATTR_DEVICE_ID), +) + + def _async_get_entries_by_name(current_entries): """Return a dict of the entries by name.""" @@ -356,7 +365,7 @@ def _async_register_events_and_services(hass: HomeAssistant): hass.http.register_view(HomeKitPairingQRView) async def async_handle_homekit_reset_accessory(service): - """Handle start HomeKit service call.""" + """Handle reset accessory HomeKit service call.""" for entry_id in hass.data[DOMAIN]: if HOMEKIT not in hass.data[DOMAIN][entry_id]: continue @@ -378,6 +387,44 @@ def _async_register_events_and_services(hass: HomeAssistant): schema=RESET_ACCESSORY_SERVICE_SCHEMA, ) + async def async_handle_homekit_unpair(service): + """Handle unpair HomeKit service call.""" + referenced = await async_extract_referenced_entity_ids(hass, service) + dev_reg = device_registry.async_get(hass) + for device_id in referenced.referenced_devices: + dev_reg_ent = dev_reg.async_get(device_id) + if not dev_reg_ent: + raise HomeAssistantError(f"No device found for device id: {device_id}") + macs = [ + cval + for ctype, cval in dev_reg_ent.connections + if ctype == device_registry.CONNECTION_NETWORK_MAC + ] + domain_data = hass.data[DOMAIN] + matching_instances = [ + domain_data[entry_id][HOMEKIT] + for entry_id in domain_data + if HOMEKIT in domain_data[entry_id] + and domain_data[entry_id][HOMEKIT].driver + and device_registry.format_mac( + domain_data[entry_id][HOMEKIT].driver.state.mac + ) + in macs + ] + if not matching_instances: + raise HomeAssistantError( + f"No homekit accessory found for device id: {device_id}" + ) + for homekit in matching_instances: + homekit.async_unpair() + + hass.services.async_register( + DOMAIN, + SERVICE_HOMEKIT_UNPAIR, + async_handle_homekit_unpair, + schema=UNPAIR_SERVICE_SCHEMA, + ) + async def async_handle_homekit_service_start(service): """Handle start HomeKit service call.""" tasks = [] @@ -639,7 +686,11 @@ class HomeKit: if self.driver.state.paired: return + self._async_show_setup_message() + @callback + def _async_show_setup_message(self): + """Show the pairing setup message.""" show_setup_message( self.hass, self._entry_id, @@ -648,6 +699,16 @@ class HomeKit: self.driver.accessory.xhm_uri(), ) + @callback + def async_unpair(self): + """Remove all pairings for an accessory so it can be repaired.""" + state = self.driver.state + for client_uuid in list(state.paired_clients): + state.remove_paired_client(client_uuid) + self.driver.async_persist() + self.driver.async_update_advertisement() + self._async_show_setup_message() + @callback def _async_register_bridge(self): """Register the bridge as a device so homekit_controller and exclude it from discovery.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 37788f9dca7..4fecd64b2b2 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -99,6 +99,7 @@ HOMEKIT_MODES = [HOMEKIT_MODE_BRIDGE, HOMEKIT_MODE_ACCESSORY] # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = "start" SERVICE_HOMEKIT_RESET_ACCESSORY = "reset_accessory" +SERVICE_HOMEKIT_UNPAIR = "unpair" # #### String Constants #### BRIDGE_MODEL = "Bridge" diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index 315a612241f..68e7804697b 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -14,3 +14,9 @@ reset_accessory: target: entity: {} +unpair: + name: Unpair an accessory or bridge + description: Forcefully remove all pairings from an accessory to allow re-pairing. Use this service if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost. + target: + device: + integration: homekit diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index ba34830f381..6539a7137d3 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -35,11 +35,13 @@ from homeassistant.components.homekit.const import ( HOMEKIT_MODE_BRIDGE, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, + SERVICE_HOMEKIT_UNPAIR, ) from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, @@ -52,7 +54,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_ON, ) -from homeassistant.core import State +from homeassistant.core import HomeAssistantError, State from homeassistant.helpers import device_registry from homeassistant.helpers.entityfilter import ( CONF_EXCLUDE_DOMAINS, @@ -668,6 +670,126 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): homekit.status = STATUS_READY +async def test_homekit_unpair(hass, device_reg, mock_zeroconf): + """Test unpairing HomeKit accessories.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + hass.states.async_set("light.demo", "on") + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + homekit.bridge.accessories = {aid: acc_mock} + homekit.status = STATUS_RUNNING + + state = homekit.driver.state + state.paired_clients = {"client1": "any"} + formatted_mac = device_registry.format_mac(state.mac) + hk_bridge_dev = device_reg.async_get_device( + {}, {(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)} + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_UNPAIR, + {ATTR_DEVICE_ID: hk_bridge_dev.id}, + blocking=True, + ) + await hass.async_block_till_done() + assert state.paired_clients == {} + homekit.status = STATUS_STOPPED + + +async def test_homekit_unpair_missing_device_id(hass, device_reg, mock_zeroconf): + """Test unpairing HomeKit accessories with invalid device id.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + hass.states.async_set("light.demo", "on") + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + homekit.bridge.accessories = {aid: acc_mock} + homekit.status = STATUS_RUNNING + + state = homekit.driver.state + state.paired_clients = {"client1": "any"} + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_UNPAIR, + {ATTR_DEVICE_ID: "notvalid"}, + blocking=True, + ) + await hass.async_block_till_done() + state.paired_clients = {"client1": "any"} + homekit.status = STATUS_STOPPED + + +async def test_homekit_unpair_not_homekit_device(hass, device_reg, mock_zeroconf): + """Test unpairing HomeKit accessories with a non-homekit device id.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + not_homekit_entry = MockConfigEntry( + domain="not_homekit", data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entity_id = "light.demo" + hass.states.async_set("light.demo", "on") + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( + "pyhap.accessory_driver.AccessoryDriver.async_start" + ): + await async_init_entry(hass, entry) + + acc_mock = MagicMock() + acc_mock.entity_id = entity_id + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + homekit.bridge.accessories = {aid: acc_mock} + homekit.status = STATUS_RUNNING + + device_entry = device_reg.async_get_or_create( + config_entry_id=not_homekit_entry.entry_id, + sw_version="0.16.0", + model="Powerwall 2", + manufacturer="Tesla", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + state = homekit.driver.state + state.paired_clients = {"client1": "any"} + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_HOMEKIT_UNPAIR, + {ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + await hass.async_block_till_done() + state.paired_clients = {"client1": "any"} + homekit.status = STATUS_STOPPED + + async def test_homekit_reset_accessories_not_supported(hass, mock_zeroconf): """Test resetting HomeKit accessories with an unsupported entity.""" await async_setup_component(hass, "persistent_notification", {}) From ff781583fc268ac0aa5d2450de8c037795296668 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 22 Jul 2021 12:59:39 +0200 Subject: [PATCH 505/818] Remove energy attributes from switch platform in devolo Home Control (#53335) --- .../components/devolo_home_control/switch.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index dcfa22db692..c9dabf23c39 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -54,25 +54,12 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): self._unique_id ) self._is_on: bool = self._binary_switch_property.state - self._consumption: float | None - - if hasattr(self._device_instance, "consumption_property"): - self._consumption = self._device_instance.consumption_property.get( - self._unique_id.replace("BinarySwitch", "Meter") - ).current - else: - self._consumption = None @property def is_on(self) -> bool: """Return the state.""" return self._is_on - @property - def current_power_w(self) -> float | None: - """Return the current consumption.""" - return self._consumption - def turn_on(self, **kwargs: Any) -> None: """Switch on the device.""" self._is_on = True @@ -87,10 +74,6 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): """Update the binary switch state and consumption.""" if message[0].startswith("devolo.BinarySwitch"): self._is_on = self._device_instance.binary_switch_property[message[0]].state - elif message[0].startswith("devolo.Meter"): - self._consumption = self._device_instance.consumption_property[ - message[0] - ].current else: self._generic_message(message) self.schedule_update_ha_state() From f009b1442ff67112430486a7e88104af9cb4da2c Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Thu, 22 Jul 2021 14:40:39 +0300 Subject: [PATCH 506/818] Switch wirelesstag to use cloud push (#50984) --- CODEOWNERS | 1 + .../components/wirelesstag/__init__.py | 138 +++++------------- .../components/wirelesstag/binary_sensor.py | 7 +- .../components/wirelesstag/manifest.json | 6 +- .../components/wirelesstag/sensor.py | 8 +- requirements_all.txt | 2 +- 6 files changed, 49 insertions(+), 113 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 28dc16e7342..249a00a796e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -565,6 +565,7 @@ homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @esev homeassistant/components/wiffi/* @mampfes homeassistant/components/wilight/* @leofig-rj +homeassistant/components/wirelesstag/* @sergeymaysak homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck homeassistant/components/wolflink/* @adamkrol93 diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 4e3ace38411..519663d1261 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -3,9 +3,8 @@ import logging from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from wirelesstagpy import NotificationConfig as NC, WirelessTags, WirelessTagsException +from wirelesstagpy import WirelessTags, WirelessTagsException -from homeassistant import util from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, @@ -67,11 +66,6 @@ class WirelessTagPlatform: self.tags = {} self._local_base_url = None - @property - def tag_manager_macs(self): - """Return list of tag managers mac addresses in user account.""" - return self.api.mac_addresses - def load_tags(self): """Load tags from remote server.""" self.tags = self.api.load_tags() @@ -91,97 +85,44 @@ class WirelessTagPlatform: if disarm_func is not None: disarm_func(switch.tag_id, switch.tag_manager_mac) - def make_notifications(self, binary_sensors, mac): - """Create configurations for push notifications.""" - _LOGGER.info("Creating configurations for push notifications") - configs = [] + def start_monitoring(self): + """Start monitoring push events.""" - bi_url = self.binary_event_callback_url - for bi_sensor in binary_sensors: - configs.extend(bi_sensor.event.build_notifications(bi_url, mac)) - - update_url = self.update_callback_url - - update_config = NC.make_config_for_update_event(update_url, mac) - - configs.append(update_config) - return configs - - def install_push_notifications(self, binary_sensors): - """Register local push notification from tag manager.""" - _LOGGER.info("Registering local push notifications") - for mac in self.tag_manager_macs: - configs = self.make_notifications(binary_sensors, mac) - # install notifications for all tags in tag manager - # specified by mac - result = self.api.install_push_notification(0, configs, True, mac) - if not result: - self.hass.components.persistent_notification.create( - "Error: failed to install local push notifications
", - title="Wireless Sensor Tag Setup Local Push Notifications", - notification_id="wirelesstag_failed_push_notification", - ) - else: - _LOGGER.info( - "Installed push notifications for all tags in %s", - mac, - ) - - @property - def local_base_url(self): - """Define base url of hass in local network.""" - if self._local_base_url is None: - self._local_base_url = f"http://{util.get_local_ip()}" - - port = self.hass.config.api.port - if port is not None: - self._local_base_url += f":{port}" - return self._local_base_url - - @property - def update_callback_url(self): - """Return url for local push notifications(update event).""" - return f"{self.local_base_url}/api/events/wirelesstag_update_tags" - - @property - def binary_event_callback_url(self): - """Return url for local push notifications(binary event).""" - return f"{self.local_base_url}/api/events/wirelesstag_binary_event" - - def handle_update_tags_event(self, event): - """Handle push event from wireless tag manager.""" - _LOGGER.info("Push notification for update arrived: %s", event) - try: - tag_id = event.data.get("id") - mac = event.data.get("mac") - dispatcher_send(self.hass, SIGNAL_TAG_UPDATE.format(tag_id, mac), event) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error( - "Unable to handle tag update event:\ - %s error: %s", - str(event), - str(ex), + def push_callback(tags_spec, event_spec): + """Handle push update.""" + _LOGGER.debug( + "Push notification arrived: %s, events: %s", tags_spec, event_spec ) + for uuid, tag in tags_spec.items(): + try: + tag_id = tag.tag_id + mac = tag.tag_manager_mac + _LOGGER.debug("Push notification for tag update arrived: %s", tag) + dispatcher_send( + self.hass, SIGNAL_TAG_UPDATE.format(tag_id, mac), tag + ) + if uuid in event_spec: + events = event_spec[uuid] + for event in events: + _LOGGER.debug( + "Push notification for binary event arrived: %s", event + ) + dispatcher_send( + self.hass, + SIGNAL_BINARY_EVENT_UPDATE.format( + tag_id, event.type, mac + ), + tag, + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error( + "Unable to handle tag update:\ + %s error: %s", + str(tag), + str(ex), + ) - def handle_binary_event(self, event): - """Handle push notifications for binary (on/off) events.""" - _LOGGER.info("Push notification for binary event arrived: %s", event) - try: - tag_id = event.data.get("id") - event_type = event.data.get("type") - mac = event.data.get("mac") - dispatcher_send( - self.hass, - SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac), - event, - ) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error( - "Unable to handle tag binary event:\ - %s error: %s", - str(event), - str(ex), - ) + self.api.start_monitoring(push_callback) def setup(hass, config): @@ -195,6 +136,7 @@ def setup(hass, config): platform = WirelessTagPlatform(hass, wirelesstags) platform.load_tags() + platform.start_monitoring() hass.data[DOMAIN] = platform except (ConnectTimeout, HTTPError, WirelessTagsException) as ex: _LOGGER.error("Unable to connect to wirelesstag.net service: %s", str(ex)) @@ -205,12 +147,6 @@ def setup(hass, config): ) return False - # listen to custom events - hass.bus.listen( - "wirelesstag_update_tags", hass.data[DOMAIN].handle_update_tags_event - ) - hass.bus.listen("wirelesstag_binary_event", hass.data[DOMAIN].handle_binary_event) - return True diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index ef97867e829..da901f31cd6 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -81,7 +81,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors.append(WirelessTagBinarySensor(platform, tag, sensor_type)) add_entities(sensors, True) - hass.add_job(platform.install_push_notifications, sensors) class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorEntity): @@ -134,8 +133,8 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorEntity): return self.principal_value @callback - def _on_binary_event_callback(self, event): + def _on_binary_event_callback(self, new_tag): """Update state from arrived push notification.""" - # state should be 'on' or 'off' - self._state = event.data.get("state") + self._tag = new_tag + self._state = self.updated_state_value() self.async_write_ha_state() diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index fd18235c994..37c1b82cba9 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -2,7 +2,7 @@ "domain": "wirelesstag", "name": "Wireless Sensor Tags", "documentation": "https://www.home-assistant.io/integrations/wirelesstag", - "requirements": ["wirelesstagpy==0.4.1"], - "codeowners": [], - "iot_class": "local_push" + "requirements": ["wirelesstagpy==0.5.0"], + "codeowners": ["@sergeymaysak"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index cc0ce0cb888..de70efda424 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -108,9 +108,9 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): return self._tag.sensor[self._sensor_type] @callback - def _update_tag_info_callback(self, event): + def _update_tag_info_callback(self, new_tag): """Handle push notification sent by tag manager.""" - _LOGGER.debug("Entity to update state: %s event data: %s", self, event.data) - new_value = self._sensor.value_from_update_event(event.data) - self._state = self.decorate_value(new_value) + _LOGGER.debug("Entity to update state: %s with new tag: %s", self, new_tag) + self._tag = new_tag + self._state = self.updated_state_value() self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 32223f0fdfb..d904a1187b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,7 +2372,7 @@ webexteamssdk==1.1.1 wiffi==1.0.1 # homeassistant.components.wirelesstag -wirelesstagpy==0.4.1 +wirelesstagpy==0.5.0 # homeassistant.components.withings withings-api==2.3.2 From d3e77e00e14267ee5869cd198fbbdfc2344f51cd Mon Sep 17 00:00:00 2001 From: sillyfrog <816454+sillyfrog@users.noreply.github.com> Date: Thu, 22 Jul 2021 22:40:33 +1000 Subject: [PATCH 507/818] Add Automate Pulse Hub v2 support (#39501) Co-authored-by: Franck Nijhof Co-authored-by: Sillyfrog --- .coveragerc | 6 + CODEOWNERS | 1 + homeassistant/components/automate/__init__.py | 36 +++++ homeassistant/components/automate/base.py | 93 +++++++++++ .../components/automate/config_flow.py | 37 +++++ homeassistant/components/automate/const.py | 6 + homeassistant/components/automate/cover.py | 147 ++++++++++++++++++ homeassistant/components/automate/helpers.py | 46 ++++++ homeassistant/components/automate/hub.py | 89 +++++++++++ .../components/automate/manifest.json | 13 ++ .../components/automate/strings.json | 19 +++ .../components/automate/translations/en.json | 19 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/automate/__init__.py | 1 + tests/components/automate/test_config_flow.py | 69 ++++++++ 17 files changed, 589 insertions(+) create mode 100644 homeassistant/components/automate/__init__.py create mode 100644 homeassistant/components/automate/base.py create mode 100644 homeassistant/components/automate/config_flow.py create mode 100644 homeassistant/components/automate/const.py create mode 100644 homeassistant/components/automate/cover.py create mode 100644 homeassistant/components/automate/helpers.py create mode 100644 homeassistant/components/automate/hub.py create mode 100644 homeassistant/components/automate/manifest.json create mode 100644 homeassistant/components/automate/strings.json create mode 100644 homeassistant/components/automate/translations/en.json create mode 100644 tests/components/automate/__init__.py create mode 100644 tests/components/automate/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2693080986b..4be3d2e5f01 100644 --- a/.coveragerc +++ b/.coveragerc @@ -75,6 +75,12 @@ omit = homeassistant/components/asuswrt/router.py homeassistant/components/aten_pe/* homeassistant/components/atome/* + homeassistant/components/automate/__init__.py + homeassistant/components/automate/base.py + homeassistant/components/automate/const.py + homeassistant/components/automate/cover.py + homeassistant/components/automate/helpers.py + homeassistant/components/automate/hub.py homeassistant/components/aurora/__init__.py homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 249a00a796e..0e92885d247 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -56,6 +56,7 @@ homeassistant/components/august/* @bdraco homeassistant/components/aurora/* @djtimca homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core +homeassistant/components/automate/* @sillyfrog homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @ahayworth @danielsjf diff --git a/homeassistant/components/automate/__init__.py b/homeassistant/components/automate/__init__.py new file mode 100644 index 00000000000..c4f34d96a05 --- /dev/null +++ b/homeassistant/components/automate/__init__.py @@ -0,0 +1,36 @@ +"""The Automate Pulse Hub v2 integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .hub import PulseHub + +PLATFORMS = ["cover"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Automate Pulse Hub v2 from a config entry.""" + hub = PulseHub(hass, entry) + + if not await hub.async_setup(): + return False + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = hub + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hub = hass.data[DOMAIN][entry.entry_id] + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + if not await hub.async_reset(): + return False + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/automate/base.py b/homeassistant/components/automate/base.py new file mode 100644 index 00000000000..de37933e54d --- /dev/null +++ b/homeassistant/components/automate/base.py @@ -0,0 +1,93 @@ +"""Base class for Automate Roller Blinds.""" +import logging + +import aiopulse2 + +from homeassistant.core import callback +from homeassistant.helpers import entity +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg + +from .const import AUTOMATE_ENTITY_REMOVE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AutomateBase(entity.Entity): + """Base representation of an Automate roller.""" + + def __init__(self, roller: aiopulse2.Roller) -> None: + """Initialize the roller.""" + self.roller = roller + + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + return self.roller.online and self.roller.hub.connected + + async def async_remove_and_unregister(self): + """Unregister from entity and device registry and call entity remove function.""" + _LOGGER.info("Removing %s %s", self.__class__.__name__, self.unique_id) + + ent_registry = await get_ent_reg(self.hass) + if self.entity_id in ent_registry.entities: + ent_registry.async_remove(self.entity_id) + + dev_registry = await get_dev_reg(self.hass) + device = dev_registry.async_get_device( + identifiers={(DOMAIN, self.unique_id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, remove_config_entry_id=self.registry_entry.config_entry_id + ) + + await self.async_remove() + + async def async_added_to_hass(self): + """Entity has been added to hass.""" + self.roller.callback_subscribe(self.notify_update) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + AUTOMATE_ENTITY_REMOVE.format(self.roller.id), + self.async_remove_and_unregister, + ) + ) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + self.roller.callback_unsubscribe(self.notify_update) + + @callback + def notify_update(self, roller: aiopulse2.Roller): + """Write updated device state information.""" + _LOGGER.debug( + "Device update notification received: %s (%r)", roller.id, roller.name + ) + self.async_write_ha_state() + + @property + def should_poll(self): + """Report that Automate entities do not need polling.""" + return False + + @property + def unique_id(self): + """Return the unique ID of this roller.""" + return self.roller.id + + @property + def name(self): + """Return the name of roller.""" + return self.roller.name + + @property + def device_info(self): + """Return the device info.""" + attrs = { + "identifiers": {(DOMAIN, self.roller.id)}, + } + return attrs diff --git a/homeassistant/components/automate/config_flow.py b/homeassistant/components/automate/config_flow.py new file mode 100644 index 00000000000..45d3a5b9349 --- /dev/null +++ b/homeassistant/components/automate/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow for Automate Pulse Hub v2 integration.""" +import logging + +import aiopulse2 +import voluptuous as vol + +from homeassistant import config_entries + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Automate Pulse Hub v2.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step once we have info from the user.""" + if user_input is not None: + try: + hub = aiopulse2.Hub(user_input["host"]) + await hub.test() + title = hub.name + except Exception: # pylint: disable=broad-except + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "cannot_connect"}, + ) + + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/automate/const.py b/homeassistant/components/automate/const.py new file mode 100644 index 00000000000..0c1dc1bd2e5 --- /dev/null +++ b/homeassistant/components/automate/const.py @@ -0,0 +1,6 @@ +"""Constants for the Automate Pulse Hub v2 integration.""" + +DOMAIN = "automate" + +AUTOMATE_HUB_UPDATE = "automate_hub_update_{}" +AUTOMATE_ENTITY_REMOVE = "automate_entity_remove_{}" diff --git a/homeassistant/components/automate/cover.py b/homeassistant/components/automate/cover.py new file mode 100644 index 00000000000..86dcda10adf --- /dev/null +++ b/homeassistant/components/automate/cover.py @@ -0,0 +1,147 @@ +"""Support for Automate Roller Blinds.""" +import aiopulse2 + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_SHADE, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .base import AutomateBase +from .const import AUTOMATE_HUB_UPDATE, DOMAIN +from .helpers import async_add_automate_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Automate Rollers from a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + current = set() + + @callback + def async_add_automate_covers(): + async_add_automate_entities( + hass, AutomateCover, config_entry, current, async_add_entities + ) + + hub.cleanup_callbacks.append( + async_dispatcher_connect( + hass, + AUTOMATE_HUB_UPDATE.format(config_entry.entry_id), + async_add_automate_covers, + ) + ) + + +class AutomateCover(AutomateBase, CoverEntity): + """Representation of a Automate cover device.""" + + @property + def current_cover_position(self): + """Return the current position of the roller blind. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = None + if self.roller.closed_percent is not None: + position = 100 - self.roller.closed_percent + return position + + @property + def current_cover_tilt_position(self): + """Return the current tilt of the roller blind. + + None is unknown, 0 is closed, 100 is fully open. + """ + return None + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = 0 + if self.current_cover_position is not None: + supported_features |= ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + if self.current_cover_tilt_position is not None: + supported_features |= ( + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION + ) + + return supported_features + + @property + def device_info(self): + """Return the device info.""" + attrs = super().device_info + attrs["manufacturer"] = "Automate" + attrs["model"] = self.roller.devicetype + attrs["sw_version"] = self.roller.version + attrs["via_device"] = (DOMAIN, self.roller.hub.id) + attrs["name"] = self.name + return attrs + + @property + def device_class(self): + """Class of the cover, a shade.""" + return DEVICE_CLASS_SHADE + + @property + def is_opening(self): + """Is cover opening/moving up.""" + return self.roller.action == aiopulse2.MovingAction.up + + @property + def is_closing(self): + """Is cover closing/moving down.""" + return self.roller.action == aiopulse2.MovingAction.down + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.roller.closed_percent == 100 + + async def async_close_cover(self, **kwargs): + """Close the roller.""" + await self.roller.move_down() + + async def async_open_cover(self, **kwargs): + """Open the roller.""" + await self.roller.move_up() + + async def async_stop_cover(self, **kwargs): + """Stop the roller.""" + await self.roller.move_stop() + + async def async_set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position.""" + await self.roller.move_to(100 - kwargs[ATTR_POSITION]) + + async def async_close_cover_tilt(self, **kwargs): + """Close the roller.""" + await self.roller.move_down() + + async def async_open_cover_tilt(self, **kwargs): + """Open the roller.""" + await self.roller.move_up() + + async def async_stop_cover_tilt(self, **kwargs): + """Stop the roller.""" + await self.roller.move_stop() + + async def async_set_cover_tilt(self, **kwargs): + """Tilt the roller shutter to a specific position.""" + await self.roller.move_to(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/automate/helpers.py b/homeassistant/components/automate/helpers.py new file mode 100644 index 00000000000..92130eeb79b --- /dev/null +++ b/homeassistant/components/automate/helpers.py @@ -0,0 +1,46 @@ +"""Helper functions for Automate Pulse.""" +import logging + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_add_automate_entities( + hass, entity_class, config_entry, current, async_add_entities +): + """Add any new entities.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + _LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) + + api = hub.api.rollers + + new_items = [] + for unique_id, roller in api.items(): + if unique_id not in current: + _LOGGER.debug("New %s %s", entity_class.__name__, unique_id) + new_item = entity_class(roller) + current.add(unique_id) + new_items.append(new_item) + + async_add_entities(new_items) + + +async def update_devices(hass, config_entry, api): + """Tell hass that device info has been updated.""" + dev_registry = await get_dev_reg(hass) + + for api_item in api.values(): + # Update Device name + device = dev_registry.async_get_device( + identifiers={(DOMAIN, api_item.id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, + name=api_item.name, + ) diff --git a/homeassistant/components/automate/hub.py b/homeassistant/components/automate/hub.py new file mode 100644 index 00000000000..78e1b5873fa --- /dev/null +++ b/homeassistant/components/automate/hub.py @@ -0,0 +1,89 @@ +"""Code to handle a Pulse Hub.""" +from __future__ import annotations + +import asyncio +import logging + +import aiopulse2 + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import AUTOMATE_ENTITY_REMOVE, AUTOMATE_HUB_UPDATE +from .helpers import update_devices + +_LOGGER = logging.getLogger(__name__) + + +class PulseHub: + """Manages a single Pulse Hub.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.api: aiopulse2.Hub | None = None + self.tasks = [] + self.current_rollers = {} + self.cleanup_callbacks = [] + + @property + def title(self): + """Return the title of the hub shown in the integrations list.""" + return f"{self.api.name} ({self.api.host})" + + @property + def host(self): + """Return the host of this hub.""" + return self.config_entry.data["host"] + + async def async_setup(self): + """Set up a hub based on host parameter.""" + host = self.host + + hub = aiopulse2.Hub(host, propagate_callbacks=True) + + self.api = hub + + hub.callback_subscribe(self.async_notify_update) + self.tasks.append(asyncio.create_task(hub.run())) + + _LOGGER.debug("Hub setup complete") + return True + + async def async_reset(self): + """Reset this hub to default state.""" + for cleanup_callback in self.cleanup_callbacks: + cleanup_callback() + + # If not setup + if self.api is None: + return False + + self.api.callback_unsubscribe(self.async_notify_update) + await self.api.stop() + del self.api + self.api = None + + # Wait for any running tasks to complete + await asyncio.wait(self.tasks) + + return True + + async def async_notify_update(self, hub=None): + """Evaluate entities when hub reports that update has occurred.""" + _LOGGER.debug("Hub {self.title} updated") + + await update_devices(self.hass, self.config_entry, self.api.rollers) + self.hass.config_entries.async_update_entry(self.config_entry, title=self.title) + + async_dispatcher_send( + self.hass, AUTOMATE_HUB_UPDATE.format(self.config_entry.entry_id) + ) + + for unique_id in list(self.current_rollers): + if unique_id not in self.api.rollers: + _LOGGER.debug("Notifying remove of %s", unique_id) + self.current_rollers.pop(unique_id) + async_dispatcher_send( + self.hass, AUTOMATE_ENTITY_REMOVE.format(unique_id) + ) diff --git a/homeassistant/components/automate/manifest.json b/homeassistant/components/automate/manifest.json new file mode 100644 index 00000000000..071aaf1589f --- /dev/null +++ b/homeassistant/components/automate/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "automate", + "name": "Automate Pulse Hub v2", + "config_flow": true, + "iot_class": "local_push", + "documentation": "https://www.home-assistant.io/integrations/automate", + "requirements": [ + "aiopulse2==0.6.0" + ], + "codeowners": [ + "@sillyfrog" + ] +} \ No newline at end of file diff --git a/homeassistant/components/automate/strings.json b/homeassistant/components/automate/strings.json new file mode 100644 index 00000000000..8a8131f0f67 --- /dev/null +++ b/homeassistant/components/automate/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/en.json b/homeassistant/components/automate/translations/en.json new file mode 100644 index 00000000000..2ad35962b25 --- /dev/null +++ b/homeassistant/components/automate/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b88d5639783..0e7b6c52cc2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ FLOWS = [ "atag", "august", "aurora", + "automate", "awair", "axis", "azure_devops", diff --git a/requirements_all.txt b/requirements_all.txt index d904a1187b3..a43f754b202 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,6 +220,9 @@ aionotify==0.2.0 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.automate +aiopulse2==0.6.0 + # homeassistant.components.acmeda aiopulse==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2baf16de687..5732833e079 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -142,6 +142,9 @@ aiomusiccast==0.8.0 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.automate +aiopulse2==0.6.0 + # homeassistant.components.acmeda aiopulse==0.4.2 diff --git a/tests/components/automate/__init__.py b/tests/components/automate/__init__.py new file mode 100644 index 00000000000..6a87ba942e3 --- /dev/null +++ b/tests/components/automate/__init__.py @@ -0,0 +1 @@ +"""Tests for the Automate Pulse Hub v2 integration.""" diff --git a/tests/components/automate/test_config_flow.py b/tests/components/automate/test_config_flow.py new file mode 100644 index 00000000000..fea2fa995cd --- /dev/null +++ b/tests/components/automate/test_config_flow.py @@ -0,0 +1,69 @@ +"""Test the Automate Pulse Hub v2 config flow.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.automate.const import DOMAIN + + +def mock_hub(testfunc=None): + """Mock aiopulse2.Hub.""" + Hub = Mock() + Hub.name = "Name of the device" + + async def hub_test(): + if testfunc: + testfunc() + + Hub.test = hub_test + + return Hub + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch("aiopulse2.Hub", return_value=mock_hub()), patch( + "homeassistant.components.automate.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Name of the device" + assert result2["data"] == { + "host": "1.1.1.1", + } + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + def raise_error(): + raise ConnectionRefusedError + + with patch("aiopulse2.Hub", return_value=mock_hub(raise_error)): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From d371ab9deb9d20135bd74a37110f585f791c13c3 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 22 Jul 2021 08:47:30 -0400 Subject: [PATCH 508/818] Use entity class attributes for caldav (#53332) --- homeassistant/components/caldav/calendar.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 61186249c51..d27555beb2c 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -119,24 +119,13 @@ class WebDavCalendarEventDevice(CalendarEventDevice): self.data = WebDavCalendarData(calendar, days, all_day, search) self.entity_id = entity_id self._event = None - self._name = name - self._offset_reached = False - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return {"offset_reached": self._offset_reached} + self._attr_name = name @property def event(self): """Return the next upcoming event.""" return self._event - @property - def name(self): - """Return the name of the entity.""" - return self._name - async def async_get_events(self, hass, start_date, end_date): """Get all events in a specific time frame.""" return await self.data.async_get_events(hass, start_date, end_date) @@ -149,8 +138,8 @@ class WebDavCalendarEventDevice(CalendarEventDevice): self._event = event return event = calculate_offset(event, OFFSET) - self._offset_reached = is_offset_reached(event) self._event = event + self._attr_extra_state_attributes = {"offset_reached": is_offset_reached(event)} class WebDavCalendarData: From f778467d631b8eca9499c776862e6a178317aa2c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:29:50 +0200 Subject: [PATCH 509/818] Use NamedTuple - rainbird (#53329) * Use NamedTuple - rainbird * Apply suggestions from code review Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/rainbird/__init__.py | 26 +++++++++++-- .../components/rainbird/binary_sensor.py | 31 +++++++-------- homeassistant/components/rainbird/sensor.py | 38 +++++++++---------- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index d8334470b60..55ed421bd24 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,5 +1,8 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" +from __future__ import annotations + import logging +from typing import NamedTuple from pyrainbird import RainbirdController import voluptuous as vol @@ -26,10 +29,25 @@ DOMAIN = "rainbird" SENSOR_TYPE_RAINDELAY = "raindelay" SENSOR_TYPE_RAINSENSOR = "rainsensor" -# sensor_type [ description, unit, icon ] -SENSOR_TYPES = { - SENSOR_TYPE_RAINSENSOR: ["Rainsensor", None, "mdi:water"], - SENSOR_TYPE_RAINDELAY: ["Raindelay", None, "mdi:water-off"], + + +class RainBirdSensorMetadata(NamedTuple): + """Metadata for an individual RainBird sensor.""" + + name: str + icon: str + unit_of_measurement: str | None = None + + +SENSOR_TYPES: dict[str, RainBirdSensorMetadata] = { + SENSOR_TYPE_RAINSENSOR: RainBirdSensorMetadata( + "Rainsensor", + icon="mdi:water", + ), + SENSOR_TYPE_RAINDELAY: RainBirdSensorMetadata( + "Raindelay", + icon="mdi:water-off", + ), } TRIGGER_TIME_SCHEMA = vol.All( diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 62c6824f5e0..9960d7670b2 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -11,6 +11,7 @@ from . import ( SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR, SENSOR_TYPES, + RainBirdSensorMetadata, ) _LOGGER = logging.getLogger(__name__) @@ -23,19 +24,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] add_entities( - [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + [ + RainBirdSensor(controller, sensor_type, metadata) + for sensor_type, metadata in SENSOR_TYPES.items() + ], + True, ) class RainBirdSensor(BinarySensorEntity): """A sensor implementation for Rain Bird device.""" - def __init__(self, controller: RainbirdController, sensor_type): + def __init__( + self, + controller: RainbirdController, + sensor_type, + metadata: RainBirdSensorMetadata, + ): """Initialize the Rain Bird sensor.""" self._sensor_type = sensor_type self._controller = controller - self._name = SENSOR_TYPES[self._sensor_type][0] - self._icon = SENSOR_TYPES[self._sensor_type][2] + + self._attr_name = metadata.name + self._attr_icon = metadata.icon self._state = None @property @@ -45,20 +56,10 @@ class RainBirdSensor(BinarySensorEntity): def update(self): """Get the latest data and updates the states.""" - _LOGGER.debug("Updating sensor: %s", self._name) + _LOGGER.debug("Updating sensor: %s", self.name) state = None if self._sensor_type == SENSOR_TYPE_RAINSENSOR: state = self._controller.get_rain_sensor_state() elif self._sensor_type == SENSOR_TYPE_RAINDELAY: state = self._controller.get_rain_delay() self._state = None if state is None else bool(state) - - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def icon(self): - """Return icon.""" - return self._icon diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 2c542dc12a9..36c3d50e1c1 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -11,6 +11,7 @@ from . import ( SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR, SENSOR_TYPES, + RainBirdSensorMetadata, ) _LOGGER = logging.getLogger(__name__) @@ -24,20 +25,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] add_entities( - [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + [ + RainBirdSensor(controller, sensor_type, metadata) + for sensor_type, metadata in SENSOR_TYPES.items() + ], + True, ) class RainBirdSensor(SensorEntity): """A sensor implementation for Rain Bird device.""" - def __init__(self, controller: RainbirdController, sensor_type): + def __init__( + self, + controller: RainbirdController, + sensor_type, + metadata: RainBirdSensorMetadata, + ): """Initialize the Rain Bird sensor.""" self._sensor_type = sensor_type self._controller = controller - self._name = SENSOR_TYPES[self._sensor_type][0] - self._icon = SENSOR_TYPES[self._sensor_type][2] - self._unit_of_measurement = SENSOR_TYPES[self._sensor_type][1] + + self._attr_name = metadata.name + self._attr_icon = metadata.icon + self._attr_unit_of_measurement = metadata.unit_of_measurement self._state = None @property @@ -47,23 +58,8 @@ class RainBirdSensor(SensorEntity): def update(self): """Get the latest data and updates the states.""" - _LOGGER.debug("Updating sensor: %s", self._name) + _LOGGER.debug("Updating sensor: %s", self.name) if self._sensor_type == SENSOR_TYPE_RAINSENSOR: self._state = self._controller.get_rain_sensor_state() elif self._sensor_type == SENSOR_TYPE_RAINDELAY: self._state = self._controller.get_rain_delay() - - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return icon.""" - return self._icon From 258162d9331087b618f0d2a7dba4ede1ef74aabc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 22 Jul 2021 16:35:19 +0200 Subject: [PATCH 510/818] Upgrade wled to 0.7.3 (#53340) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 348109f6b87..dbe13fe56ca 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.7.1"], + "requirements": ["wled==0.7.3"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index a43f754b202..d6916cc0122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,7 +2381,7 @@ wirelesstagpy==0.5.0 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.1 +wled==0.7.3 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5732833e079..8e8fa192141 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1305,7 +1305,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.1 +wled==0.7.3 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 24e07bc1545ac69c23e93b164fc6be0e0d1a681b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 22 Jul 2021 18:19:39 +0200 Subject: [PATCH 511/818] Fritzbox enable temp sensor (#52558) --- homeassistant/components/fritzbox/__init__.py | 19 ++++++ .../components/fritzbox/binary_sensor.py | 2 + homeassistant/components/fritzbox/climate.py | 2 + homeassistant/components/fritzbox/model.py | 1 + homeassistant/components/fritzbox/sensor.py | 18 +++--- homeassistant/components/fritzbox/switch.py | 2 + tests/components/fritzbox/__init__.py | 57 +++++++----------- tests/components/fritzbox/const.py | 20 +++++++ .../components/fritzbox/test_binary_sensor.py | 18 +++++- tests/components/fritzbox/test_climate.py | 18 +++++- tests/components/fritzbox/test_config_flow.py | 6 +- tests/components/fritzbox/test_init.py | 60 ++++++++++++++++++- tests/components/fritzbox/test_sensor.py | 20 +++++-- tests/components/fritzbox/test_switch.py | 25 ++++++-- 14 files changed, 205 insertions(+), 63 deletions(-) create mode 100644 tests/components/fritzbox/const.py diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 124719b93c1..087faeb2be9 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -6,6 +6,7 @@ from datetime import timedelta from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError import requests +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -16,10 +17,12 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + TEMP_CELSIUS, ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -81,6 +84,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() + def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + if ( + entry.unit_of_measurement == TEMP_CELSIUS + and "_temperature" not in entry.unique_id + ): + new_unique_id = f"{entry.unique_id}_temperature" + LOGGER.info( + "Migrating unique_id [%s] to [%s]", entry.unique_id, new_unique_id + ) + return {"new_unique_id": new_unique_id} + return None + + await async_migrate_entries(hass, entry.entry_id, _update_unique_id) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def logout_fritzbox(event: Event) -> None: @@ -123,6 +141,7 @@ class FritzBoxEntity(CoordinatorEntity): self._unique_id = entity_info[ATTR_ENTITY_ID] self._unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] self._device_class = entity_info[ATTR_DEVICE_CLASS] + self._attr_state_class = entity_info[ATTR_STATE_CLASS] @property def device(self) -> FritzhomeDevice: diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 242e3d6e644..f6dbaed97cf 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_WINDOW, BinarySensorEntity, ) +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -37,6 +38,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}", ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: DEVICE_CLASS_WINDOW, + ATTR_STATE_CLASS: None, }, coordinator, ain, diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index c50e0d4f270..0551c5e0455 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -13,6 +13,7 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -74,6 +75,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}", ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, + ATTR_STATE_CLASS: None, }, coordinator, ain, diff --git a/homeassistant/components/fritzbox/model.py b/homeassistant/components/fritzbox/model.py index 1cde7b9ca70..0e401a75be3 100644 --- a/homeassistant/components/fritzbox/model.py +++ b/homeassistant/components/fritzbox/model.py @@ -11,6 +11,7 @@ class EntityInfo(TypedDict): entity_id: str unit_of_measurement: str | None device_class: str | None + state_class: str | None class ClimateExtraAttributes(TypedDict, total=False): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index db50776d69c..0a83e3ba60c 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,7 +1,11 @@ """Support for AVM FRITZ!SmartHome temperature sensor only devices.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -34,18 +38,15 @@ async def async_setup_entry( coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] for ain, device in coordinator.data.items(): - if ( - device.has_temperature_sensor - and not device.has_switch - and not device.has_thermostat - ): + if device.has_temperature_sensor and not device.has_thermostat: entities.append( FritzBoxTempSensor( { - ATTR_NAME: f"{device.name}", - ATTR_ENTITY_ID: f"{device.ain}", + ATTR_NAME: f"{device.name} Temperature", + ATTR_ENTITY_ID: f"{device.ain}_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, coordinator, ain, @@ -60,6 +61,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}_battery", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_STATE_CLASS: None, }, coordinator, ain, diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 82581473714..22b2adf5800 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -50,6 +51,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}", ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, + ATTR_STATE_CLASS: None, }, coordinator, ain, diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index ee5d15bd1b8..3ff4b71364e 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -5,22 +5,16 @@ from typing import Any from unittest.mock import Mock from homeassistant.components.fritzbox.const import DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from .const import ( + CONF_FAKE_AIN, + CONF_FAKE_MANUFACTURER, + CONF_FAKE_NAME, + CONF_FAKE_PRODUCTNAME, +) -MOCK_CONFIG = { - DOMAIN: { - CONF_DEVICES: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] - } -} +from tests.common import MockConfigEntry async def setup_config_entry( @@ -45,27 +39,32 @@ async def setup_config_entry( return result -class FritzDeviceBinarySensorMock(Mock): +class FritzDeviceBaseMock(Mock): + """base mock of a AVM Fritz!Box binary sensor device.""" + + ain = CONF_FAKE_AIN + manufacturer = CONF_FAKE_MANUFACTURER + name = CONF_FAKE_NAME + productname = CONF_FAKE_PRODUCTNAME + + +class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): """Mock of a AVM Fritz!Box binary sensor device.""" - ain = "fake_ain" alert_state = "fake_state" + battery_level = 23 fw_version = "1.2.3" has_alarm = True has_switch = False has_temperature_sensor = False has_thermostat = False - manufacturer = "fake_manufacturer" - name = "fake_name" present = True - productname = "fake_productname" -class FritzDeviceClimateMock(Mock): +class FritzDeviceClimateMock(FritzDeviceBaseMock): """Mock of a AVM Fritz!Box climate device.""" actual_temperature = 18.0 - ain = "fake_ain" alert_state = "fake_state" battery_level = 23 battery_low = True @@ -79,19 +78,15 @@ class FritzDeviceClimateMock(Mock): has_thermostat = True holiday_active = "fake_holiday" lock = "fake_locked" - manufacturer = "fake_manufacturer" - name = "fake_name" present = True - productname = "fake_productname" summer_active = "fake_summer" target_temperature = 19.5 window_open = "fake_window" -class FritzDeviceSensorMock(Mock): +class FritzDeviceSensorMock(FritzDeviceBaseMock): """Mock of a AVM Fritz!Box sensor device.""" - ain = "fake_ain" battery_level = 23 device_lock = "fake_locked_device" fw_version = "1.2.3" @@ -100,17 +95,14 @@ class FritzDeviceSensorMock(Mock): has_temperature_sensor = True has_thermostat = False lock = "fake_locked" - manufacturer = "fake_manufacturer" - name = "fake_name" present = True - productname = "fake_productname" temperature = 1.23 -class FritzDeviceSwitchMock(Mock): +class FritzDeviceSwitchMock(FritzDeviceBaseMock): """Mock of a AVM Fritz!Box switch device.""" - ain = "fake_ain" + battery_level = None device_lock = "fake_locked_device" energy = 1234 fw_version = "1.2.3" @@ -120,9 +112,6 @@ class FritzDeviceSwitchMock(Mock): has_thermostat = False switch_state = "fake_state" lock = "fake_locked" - manufacturer = "fake_manufacturer" - name = "fake_name" power = 5678 present = True - productname = "fake_productname" - temperature = 135 + temperature = 1.23 diff --git a/tests/components/fritzbox/const.py b/tests/components/fritzbox/const.py new file mode 100644 index 00000000000..1b8bc927800 --- /dev/null +++ b/tests/components/fritzbox/const.py @@ -0,0 +1,20 @@ +"""Constants for fritzbox tests.""" +from homeassistant.components.fritzbox.const import DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +MOCK_CONFIG = { + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] + } +} + +CONF_FAKE_NAME = "fake_name" +CONF_FAKE_AIN = "fake_ain" +CONF_FAKE_MANUFACTURER = "fake_manufacturer" +CONF_FAKE_PRODUCTNAME = "fake_productname" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 7a2d2347004..f4e32fbe3df 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -7,21 +7,25 @@ from requests.exceptions import HTTPError from homeassistant.components.binary_sensor import DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, + PERCENTAGE, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceBinarySensorMock, setup_config_entry +from . import FritzDeviceBinarySensorMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed -ENTITY_ID = f"{DOMAIN}.fake_name" +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" async def test_setup(hass: HomeAssistant, fritz: Mock): @@ -34,8 +38,16 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state assert state.state == STATE_ON - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert state.attributes[ATTR_DEVICE_CLASS] == "window" + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_battery") + assert state + assert state.state == "23" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Battery" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_STATE_CLASS not in state.attributes async def test_is_off(hass: HomeAssistant, fritz: Mock): diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 59d32e18c34..30ee7130fea 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -30,21 +30,25 @@ from homeassistant.components.fritzbox.const import ( ATTR_STATE_WINDOW_OPEN, DOMAIN as FB_DOMAIN, ) +from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, + PERCENTAGE, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceClimateMock, setup_config_entry +from . import FritzDeviceClimateMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed -ENTITY_ID = f"{DOMAIN}.fake_name" +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" async def test_setup(hass: HomeAssistant, fritz: Mock): @@ -58,7 +62,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state assert state.attributes[ATTR_BATTERY_LEVEL] == 23 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18 - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT, HVAC_MODE_OFF] assert state.attributes[ATTR_MAX_TEMP] == 28 assert state.attributes[ATTR_MIN_TEMP] == 8 @@ -71,8 +75,16 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer" assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window" assert state.attributes[ATTR_TEMPERATURE] == 19.5 + assert ATTR_STATE_CLASS not in state.attributes assert state.state == HVAC_MODE_HEAT + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_battery") + assert state + assert state.state == "23" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Battery" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_STATE_CLASS not in state.attributes + async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index a9de92060ec..6d62122a871 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -21,14 +21,14 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import MOCK_CONFIG +from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import MockConfigEntry MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "fake_name", + ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", } @@ -192,7 +192,7 @@ async def test_ssdp(hass: HomeAssistant, fritz: Mock): user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "fake_name" + assert result["title"] == CONF_FAKE_NAME assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 438335868cd..ea0356c6af1 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -7,6 +7,7 @@ from pyfritzhome import LoginError from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -15,10 +16,13 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from . import MOCK_CONFIG, FritzDeviceSwitchMock, setup_config_entry +from . import FritzDeviceSwitchMock, setup_config_entry +from .const import CONF_FAKE_AIN, CONF_FAKE_NAME, MOCK_CONFIG from tests.common import MockConfigEntry @@ -38,6 +42,58 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): ] +async def test_update_unique_id(hass: HomeAssistant, fritz: Mock): + """Test unique_id update of integration.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + FB_DOMAIN, + CONF_FAKE_AIN, + unit_of_measurement=TEMP_CELSIUS, + config_entry=entry, + ) + assert entity.unique_id == CONF_FAKE_AIN + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == f"{CONF_FAKE_AIN}_temperature" + + +async def test_update_unique_id_no_change(hass: HomeAssistant, fritz: Mock): + """Test unique_id is not updated of integration.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + FB_DOMAIN, + f"{CONF_FAKE_AIN}_temperature", + unit_of_measurement=TEMP_CELSIUS, + config_entry=entry, + ) + assert entity.unique_id == f"{CONF_FAKE_AIN}_temperature" + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == f"{CONF_FAKE_AIN}_temperature" + + async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock): """Test coordinator after reboot.""" entry = MockConfigEntry( @@ -74,7 +130,7 @@ async def test_coordinator_update_after_password_change( async def test_unload_remove(hass: HomeAssistant, fritz: Mock): """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] - entity_id = f"{SWITCH_DOMAIN}.fake_name" + entity_id = f"{SWITCH_DOMAIN}.{CONF_FAKE_NAME}" entry = MockConfigEntry( domain=FB_DOMAIN, diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index c1d82a93189..664b6765c03 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -9,7 +9,11 @@ from homeassistant.components.fritzbox.const import ( ATTR_STATE_LOCKED, DOMAIN as FB_DOMAIN, ) -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -20,11 +24,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceSensorMock, setup_config_entry +from . import FritzDeviceSensorMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed -ENTITY_ID = f"{DOMAIN}.fake_name" +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" async def test_setup(hass: HomeAssistant, fritz: Mock): @@ -33,20 +38,23 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) + await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) + state = hass.states.get(f"{ENTITY_ID}_temperature") assert state assert state.state == "1.23" - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT state = hass.states.get(f"{ENTITY_ID}_battery") assert state assert state.state == "23" - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name Battery" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Battery" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_STATE_CLASS not in state.attributes async def test_update(hass: HomeAssistant, fritz: Mock): diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index cc0caeafa69..4bace3834fb 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -12,11 +12,17 @@ from homeassistant.components.fritzbox.const import ( ATTR_TOTAL_CONSUMPTION_UNIT, DOMAIN as FB_DOMAIN, ) +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) from homeassistant.components.switch import ATTR_CURRENT_POWER_W, DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, ENERGY_KILO_WATT_HOUR, SERVICE_TURN_OFF, @@ -27,11 +33,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceSwitchMock, setup_config_entry +from . import FritzDeviceSwitchMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed -ENTITY_ID = f"{DOMAIN}.fake_name" +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" async def test_setup(hass: HomeAssistant, fritz: Mock): @@ -45,13 +52,23 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state assert state.state == STATE_ON assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678 - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" - assert state.attributes[ATTR_TEMPERATURE] == "135" + assert state.attributes[ATTR_TEMPERATURE] == "1.23" assert state.attributes[ATTR_TEMPERATURE_UNIT] == TEMP_CELSIUS assert state.attributes[ATTR_TOTAL_CONSUMPTION] == "1.234" assert state.attributes[ATTR_TOTAL_CONSUMPTION_UNIT] == ENERGY_KILO_WATT_HOUR + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature") + assert state + assert state.state == "1.23" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" + assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" + assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT async def test_turn_on(hass: HomeAssistant, fritz: Mock): From c9c1c62d67929897de7897db6918887da3768fba Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 22 Jul 2021 18:24:06 +0200 Subject: [PATCH 512/818] Add state class and last reset to consumption sensor in devolo Home Control (#53337) * Add state class and last reset * Use STATE_CLASS_MEASUREMENT --- .../components/devolo_home_control/sensor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 0500fc72b0b..af67c6cd78a 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -167,6 +168,12 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): self._sensor_type = consumption self._device_class = DEVICE_CLASS_MAPPING.get(consumption) + if consumption == "total": + self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_last_reset = device_instance.consumption_property[ + element_uid + ].total_since + self._value = getattr( device_instance.consumption_property[element_uid], consumption ) @@ -183,11 +190,15 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): def _sync(self, message: tuple) -> None: """Update the consumption sensor state.""" - if message[0] == self._unique_id: + if message[0] == self._unique_id and message[2] != "total_since": self._value = getattr( self._device_instance.consumption_property[self._unique_id], self._sensor_type, ) + elif message[0] == self._unique_id and message[2] == "total_since": + self._attr_last_reset = self._device_instance.consumption_property[ + self._unique_id + ].total_since else: self._generic_message(message) self.schedule_update_ha_state() From 74023fce21159623339dda7c18fd3ab72a6dfba5 Mon Sep 17 00:00:00 2001 From: Ian Harcombe Date: Thu, 22 Jul 2021 17:24:47 +0100 Subject: [PATCH 513/818] Fix for issue #53031 (#53343) Logs from issue #53031 show that not only ints are appearing in the values for the forecast data now, so change the check from just for int, to see whether the value has a "value" attribute before dereferencing it. --- homeassistant/components/metoffice/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 1307c3aae45..1120e75c50a 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -221,7 +221,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): elif hasattr(self.coordinator.data.now, self._type): value = getattr(self.coordinator.data.now, self._type) - if not isinstance(value, int): + if hasattr(value, "value"): value = value.value return value From b2528e97b603c71cc05eb0a9dc50f6f8d67148f7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 22 Jul 2021 18:30:54 +0200 Subject: [PATCH 514/818] Making Pytest default for VS code (#53203) --- .vscode/tasks.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1308f535428..24d643b96bc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,10 +5,7 @@ "label": "Run Home Assistant Core", "type": "shell", "command": "hass -c ./config", - "group": { - "kind": "test", - "isDefault": true - }, + "group": "test", "presentation": { "reveal": "always", "panel": "new" @@ -19,7 +16,9 @@ "label": "Pytest", "type": "shell", "command": "pytest --timeout=10 tests", - "dependsOn": ["Install all Test Requirements"], + "dependsOn": [ + "Install all Test Requirements" + ], "group": { "kind": "test", "isDefault": true @@ -48,7 +47,9 @@ "label": "Pylint", "type": "shell", "command": "pylint homeassistant", - "dependsOn": ["Install all Requirements"], + "dependsOn": [ + "Install all Requirements" + ], "group": { "kind": "test", "isDefault": true From 0707792bec7169f95a6aee9d7d11a435fa4cd20b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 22 Jul 2021 13:04:02 -0500 Subject: [PATCH 515/818] Handle more Sonos snapshot restore scenarios (#53277) --- homeassistant/components/sonos/speaker.py | 65 ++++++++++++++++++----- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 3ff6627bb8a..d7bc1269ea1 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -14,7 +14,7 @@ import async_timeout from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from pysonos.events_base import Event as SonosEvent, SubscriptionBase -from pysonos.exceptions import SoCoException +from pysonos.exceptions import SoCoException, SoCoUPnPException from pysonos.music_library import MusicLibrary from pysonos.plugins.sharelink import ShareLinkPlugin from pysonos.snapshot import Snapshot @@ -25,6 +25,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as ent_reg from homeassistant.helpers.dispatcher import ( async_dispatcher_send, @@ -802,27 +803,58 @@ class SonosSpeaker: """Restore snapshots for all the speakers.""" def _restore_groups( - speakers: list[SonosSpeaker], with_group: bool + speakers: set[SonosSpeaker], with_group: bool ) -> list[list[SonosSpeaker]]: """Pause all current coordinators and restore groups.""" for speaker in (s for s in speakers if s.is_coordinator): - if speaker.media.playback_status == SONOS_STATE_PLAYING: - speaker.soco.pause() + if ( + speaker.media.playback_status == SONOS_STATE_PLAYING + and "Pause" in speaker.soco.available_actions + ): + try: + speaker.soco.pause() + except SoCoUPnPException as exc: + _LOGGER.debug( + "Pause failed during restore of %s: %s", + speaker.zone_name, + speaker.soco.available_actions, + exc_info=exc, + ) groups = [] + if not with_group: + return groups - if with_group: - # Unjoin slaves first to prevent inheritance of queues - for speaker in [s for s in speakers if not s.is_coordinator]: - if speaker.snapshot_group != speaker.sonos_group: - speaker.unjoin() + # Unjoin non-coordinator speakers not contained in the desired snapshot group + # + # If a coordinator is unjoined from its group, another speaker from the group + # will inherit the coordinator's playqueue and its own playqueue will be lost + speakers_to_unjoin = set() + for speaker in speakers: + if speaker.sonos_group == speaker.snapshot_group: + continue - # Bring back the original group topology - for speaker in (s for s in speakers if s.snapshot_group): - assert speaker.snapshot_group is not None - if speaker.snapshot_group[0] == speaker: + speakers_to_unjoin.update( + { + s + for s in speaker.sonos_group[1:] + if s not in speaker.snapshot_group + } + ) + + for speaker in speakers_to_unjoin: + speaker.unjoin() + + # Bring back the original group topology + for speaker in (s for s in speakers if s.snapshot_group): + assert speaker.snapshot_group is not None + if speaker.snapshot_group[0] == speaker: + if ( + speaker.snapshot_group != speaker.sonos_group + and speaker.snapshot_group != [speaker] + ): speaker.join(speaker.snapshot_group) - groups.append(speaker.snapshot_group.copy()) + groups.append(speaker.snapshot_group.copy()) return groups @@ -836,6 +868,11 @@ class SonosSpeaker: # Find all affected players speakers_set = {s for s in speakers if s.soco_snapshot} + if missing_snapshots := set(speakers) - speakers_set: + raise HomeAssistantError( + f"Restore failed, speakers are missing snapshots: {[s.zone_name for s in missing_snapshots]}" + ) + if with_group: for speaker in [s for s in speakers_set if s.snapshot_group]: assert speaker.snapshot_group is not None From 032cae772a5400108d0f50e52db5b7e77b53aef3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 22 Jul 2021 12:04:27 -0600 Subject: [PATCH 516/818] Bump aionotion to 3.0.2 (#53354) --- homeassistant/components/notion/__init__.py | 2 +- homeassistant/components/notion/config_flow.py | 2 +- homeassistant/components/notion/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 6fff031ae25..8acf9c24d4a 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: client = await async_get_client( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session ) except InvalidCredentialsError: LOGGER.error("Invalid username and/or password") diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 4b654e4366e..ad6d8eb9519 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -44,7 +44,7 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await async_get_client( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session + user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session ) except NotionError: return await self._show_form({"base": "invalid_auth"}) diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 191f66ee59d..378d6442e31 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -3,7 +3,7 @@ "name": "Notion", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/notion", - "requirements": ["aionotion==1.1.0"], + "requirements": ["aionotion==3.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d6916cc0122..9c9e88f22ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiomusiccast==0.8.0 aionotify==0.2.0 # homeassistant.components.notion -aionotion==1.1.0 +aionotion==3.0.2 # homeassistant.components.automate aiopulse2==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e8fa192141..c003da1711c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.8.0 # homeassistant.components.notion -aionotion==1.1.0 +aionotion==3.0.2 # homeassistant.components.automate aiopulse2==0.6.0 From 3461f61f9ff5a3dc08f00215d64920a99e543365 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 22 Jul 2021 14:11:36 -0400 Subject: [PATCH 517/818] Create APIs for Insteon panel (#49785) --- homeassistant/components/insteon/__init__.py | 3 + .../components/insteon/api/__init__.py | 44 ++ homeassistant/components/insteon/api/aldb.py | 309 +++++++++++++ .../components/insteon/api/device.py | 79 ++++ .../components/insteon/api/properties.py | 420 +++++++++++++++++ homeassistant/components/insteon/const.py | 14 + .../components/insteon/manifest.json | 2 +- homeassistant/components/insteon/schemas.py | 8 + tests/components/insteon/mock_connection.py | 11 + tests/components/insteon/mock_devices.py | 75 +++- tests/components/insteon/test_api_aldb.py | 288 ++++++++++++ tests/components/insteon/test_api_device.py | 139 ++++++ .../components/insteon/test_api_properties.py | 425 ++++++++++++++++++ tests/fixtures/insteon/aldb_data.json | 67 +++ tests/fixtures/insteon/kpl_properties.json | 66 +++ 15 files changed, 1943 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/insteon/api/__init__.py create mode 100644 homeassistant/components/insteon/api/aldb.py create mode 100644 homeassistant/components/insteon/api/device.py create mode 100644 homeassistant/components/insteon/api/properties.py create mode 100644 tests/components/insteon/mock_connection.py create mode 100644 tests/components/insteon/test_api_aldb.py create mode 100644 tests/components/insteon/test_api_device.py create mode 100644 tests/components/insteon/test_api_properties.py create mode 100644 tests/fixtures/insteon/aldb_data.json create mode 100644 tests/fixtures/insteon/kpl_properties.json diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 2e2d801e1f2..223448953b9 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady +from . import api from .const import ( CONF_CAT, CONF_DIM_STEPS, @@ -164,6 +165,8 @@ async def async_setup_entry(hass, entry): sw_version=f"{devices.modem.firmware:02x} Engine Version: {devices.modem.engine_version}", ) + api.async_load_api(hass) + asyncio.create_task(async_get_device_config(hass, entry)) return True diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py new file mode 100644 index 00000000000..3b786a38343 --- /dev/null +++ b/homeassistant/components/insteon/api/__init__.py @@ -0,0 +1,44 @@ +"""Insteon API interface for the frontend.""" + +from homeassistant.components import websocket_api +from homeassistant.core import callback + +from .aldb import ( + websocket_add_default_links, + websocket_change_aldb_record, + websocket_create_aldb_record, + websocket_get_aldb, + websocket_load_aldb, + websocket_notify_on_aldb_status, + websocket_reset_aldb, + websocket_write_aldb, +) +from .device import websocket_get_device +from .properties import ( + websocket_change_properties_record, + websocket_get_properties, + websocket_load_properties, + websocket_reset_properties, + websocket_write_properties, +) + + +@callback +def async_load_api(hass): + """Set up the web socket API.""" + websocket_api.async_register_command(hass, websocket_get_device) + + websocket_api.async_register_command(hass, websocket_get_aldb) + websocket_api.async_register_command(hass, websocket_change_aldb_record) + websocket_api.async_register_command(hass, websocket_create_aldb_record) + websocket_api.async_register_command(hass, websocket_write_aldb) + websocket_api.async_register_command(hass, websocket_load_aldb) + websocket_api.async_register_command(hass, websocket_reset_aldb) + websocket_api.async_register_command(hass, websocket_add_default_links) + websocket_api.async_register_command(hass, websocket_notify_on_aldb_status) + + websocket_api.async_register_command(hass, websocket_get_properties) + websocket_api.async_register_command(hass, websocket_change_properties_record) + websocket_api.async_register_command(hass, websocket_write_properties) + websocket_api.async_register_command(hass, websocket_load_properties) + websocket_api.async_register_command(hass, websocket_reset_properties) diff --git a/homeassistant/components/insteon/api/aldb.py b/homeassistant/components/insteon/api/aldb.py new file mode 100644 index 00000000000..881cb0bb8c7 --- /dev/null +++ b/homeassistant/components/insteon/api/aldb.py @@ -0,0 +1,309 @@ +"""Web socket API for Insteon devices.""" + +from pyinsteon import devices +from pyinsteon.constants import ALDBStatus +from pyinsteon.topics import ( + ALDB_STATUS_CHANGED, + DEVICE_LINK_CONTROLLER_CREATED, + DEVICE_LINK_RESPONDER_CREATED, +) +from pyinsteon.utils import subscribe_topic, unsubscribe_topic +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from ..const import DEVICE_ADDRESS, ID, INSTEON_DEVICE_NOT_FOUND, TYPE +from .device import async_device_name, notify_device_not_found + +ALDB_RECORD = "record" +ALDB_RECORD_SCHEMA = vol.Schema( + { + vol.Required("mem_addr"): int, + vol.Required("in_use"): bool, + vol.Required("group"): vol.Range(0, 255), + vol.Required("is_controller"): bool, + vol.Optional("highwater"): bool, + vol.Required("target"): str, + vol.Optional("target_name"): str, + vol.Required("data1"): vol.Range(0, 255), + vol.Required("data2"): vol.Range(0, 255), + vol.Required("data3"): vol.Range(0, 255), + vol.Optional("dirty"): bool, + } +) + + +async def async_aldb_record_to_dict(dev_registry, record, dirty=False): + """Convert an ALDB record to a dict.""" + return ALDB_RECORD_SCHEMA( + { + "mem_addr": record.mem_addr, + "in_use": record.is_in_use, + "is_controller": record.is_controller, + "highwater": record.is_high_water_mark, + "group": record.group, + "target": str(record.target), + "target_name": await async_device_name(dev_registry, record.target), + "data1": record.data1, + "data2": record.data2, + "data3": record.data3, + "dirty": dirty, + } + ) + + +async def async_reload_and_save_aldb(hass, device): + """Add default links to an Insteon device.""" + if device == devices.modem: + await device.aldb.async_load() + else: + await device.aldb.async_load(refresh=True) + await devices.async_save(workdir=hass.config.config_dir) + + +@websocket_api.websocket_command( + {vol.Required(TYPE): "insteon/aldb/get", vol.Required(DEVICE_ADDRESS): str} +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_aldb( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Get the All-Link Database for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + # Convert the ALDB to a dict merge in pending changes + aldb = {mem_addr: device.aldb[mem_addr] for mem_addr in device.aldb} + aldb.update(device.aldb.pending_changes) + changed_records = list(device.aldb.pending_changes.keys()) + + dev_registry = await hass.helpers.device_registry.async_get_registry() + + records = [ + await async_aldb_record_to_dict( + dev_registry, aldb[mem_addr], mem_addr in changed_records + ) + for mem_addr in aldb + ] + + connection.send_result(msg[ID], records) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/change", + vol.Required(DEVICE_ADDRESS): str, + vol.Required(ALDB_RECORD): ALDB_RECORD_SCHEMA, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_change_aldb_record( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Change an All-Link Database record for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + record = msg[ALDB_RECORD] + device.aldb.modify( + mem_addr=record["mem_addr"], + in_use=record["in_use"], + group=record["group"], + controller=record["is_controller"], + target=record["target"], + data1=record["data1"], + data2=record["data2"], + data3=record["data3"], + ) + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/create", + vol.Required(DEVICE_ADDRESS): str, + vol.Required(ALDB_RECORD): ALDB_RECORD_SCHEMA, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_create_aldb_record( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Create an All-Link Database record for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + record = msg[ALDB_RECORD] + device.aldb.add( + group=record["group"], + controller=record["is_controller"], + target=record["target"], + data1=record["data1"], + data2=record["data2"], + data3=record["data3"], + ) + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/write", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_write_aldb( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Create an All-Link Database record for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + await device.aldb.async_write() + hass.async_create_task(async_reload_and_save_aldb(hass, device)) + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/load", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_load_aldb( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Create an All-Link Database record for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + hass.async_create_task(async_reload_and_save_aldb(hass, device)) + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/reset", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_reset_aldb( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Create an All-Link Database record for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + device.aldb.clear_pending() + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/add_default_links", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_add_default_links( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add the default All-Link Database records for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + device.aldb.clear_pending() + await device.async_add_default_links() + hass.async_create_task(async_reload_and_save_aldb(hass, device)) + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/aldb/notify", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_notify_on_aldb_status( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Tell Insteon a new ALDB record was added.""" + + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + @callback + def record_added(controller, responder, group): + """Forward ALDB events to websocket.""" + forward_data = {"type": "record_loaded"} + connection.send_message(websocket_api.event_message(msg["id"], forward_data)) + + @callback + def aldb_loaded(): + """Forward ALDB loaded event to websocket.""" + forward_data = { + "type": "status_changed", + "is_loading": device.aldb.status == ALDBStatus.LOADING, + } + connection.send_message(websocket_api.event_message(msg["id"], forward_data)) + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + unsubscribe_topic(record_added, f"{DEVICE_LINK_CONTROLLER_CREATED}.{device.id}") + unsubscribe_topic(record_added, f"{DEVICE_LINK_RESPONDER_CREATED}.{device.id}") + unsubscribe_topic(aldb_loaded, f"{device.id}.{ALDB_STATUS_CHANGED}") + + forward_data = {"type": "unsubscribed"} + connection.send_message(websocket_api.event_message(msg["id"], forward_data)) + + connection.subscriptions[msg["id"]] = async_cleanup + subscribe_topic(record_added, f"{DEVICE_LINK_CONTROLLER_CREATED}.{device.id}") + subscribe_topic(record_added, f"{DEVICE_LINK_RESPONDER_CREATED}.{device.id}") + subscribe_topic(aldb_loaded, f"{device.id}.{ALDB_STATUS_CHANGED}") + + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py new file mode 100644 index 00000000000..9d77e8b765c --- /dev/null +++ b/homeassistant/components/insteon/api/device.py @@ -0,0 +1,79 @@ +"""API interface to get an Insteon device.""" + +from pyinsteon import devices +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant + +from ..const import ( + DEVICE_ID, + DOMAIN, + HA_DEVICE_NOT_FOUND, + ID, + INSTEON_DEVICE_NOT_FOUND, + TYPE, +) + + +def compute_device_name(ha_device): + """Return the HA device name.""" + return ha_device.name_by_user if ha_device.name_by_user else ha_device.name + + +def get_insteon_device_from_ha_device(ha_device): + """Return the Insteon device from an HA device.""" + for identifier in ha_device.identifiers: + if len(identifier) > 1 and identifier[0] == DOMAIN and devices[identifier[1]]: + return devices[identifier[1]] + return None + + +async def async_device_name(dev_registry, address): + """Get the Insteon device name from a device registry id.""" + ha_device = dev_registry.async_get_device( + identifiers={(DOMAIN, str(address))}, connections=set() + ) + if not ha_device: + device = devices[address] + if device: + return f"{device.description} ({device.model})" + return "" + return compute_device_name(ha_device) + + +def notify_device_not_found(connection, msg, text): + """Notify the caller that the device was not found.""" + connection.send_message( + websocket_api.error_message(msg[ID], websocket_api.const.ERR_NOT_FOUND, text) + ) + + +@websocket_api.websocket_command( + {vol.Required(TYPE): "insteon/device/get", vol.Required(DEVICE_ID): str} +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Get an Insteon device.""" + dev_registry = await hass.helpers.device_registry.async_get_registry() + ha_device = dev_registry.async_get(msg[DEVICE_ID]) + if not ha_device: + notify_device_not_found(connection, msg, HA_DEVICE_NOT_FOUND) + return + device = get_insteon_device_from_ha_device(ha_device) + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + ha_name = compute_device_name(ha_device) + device_info = { + "name": ha_name, + "address": str(device.address), + "is_battery": device.is_battery, + "aldb_status": str(device.aldb.status), + } + connection.send_result(msg[ID], device_info) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py new file mode 100644 index 00000000000..0b3b643b617 --- /dev/null +++ b/homeassistant/components/insteon/api/properties.py @@ -0,0 +1,420 @@ +"""Property update methods and schemas.""" +from itertools import chain + +from pyinsteon import devices +from pyinsteon.constants import RAMP_RATES, ResponseStatus +from pyinsteon.device_types.device_base import Device +from pyinsteon.extended_property import ( + NON_TOGGLE_MASK, + NON_TOGGLE_ON_OFF_MASK, + OFF_MASK, + ON_MASK, + RAMP_RATE, +) +from pyinsteon.utils import ramp_rate_to_seconds, seconds_to_ramp_rate +import voluptuous as vol +import voluptuous_serialize + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv + +from ..const import ( + DEVICE_ADDRESS, + ID, + INSTEON_DEVICE_NOT_FOUND, + PROPERTY_NAME, + PROPERTY_VALUE, + TYPE, +) +from .device import notify_device_not_found + +TOGGLE_ON_OFF_MODE = "toggle_on_off_mode" +NON_TOGGLE_ON_MODE = "non_toggle_on_mode" +NON_TOGGLE_OFF_MODE = "non_toggle_off_mode" +RADIO_BUTTON_GROUP_PROP = "radio_button_group_" +TOGGLE_PROP = "toggle_" +RAMP_RATE_SECONDS = list(dict.fromkeys(RAMP_RATES.values())) +RAMP_RATE_SECONDS.sort() +TOGGLE_MODES = {TOGGLE_ON_OFF_MODE: 0, NON_TOGGLE_ON_MODE: 1, NON_TOGGLE_OFF_MODE: 2} +TOGGLE_MODES_SCHEMA = { + 0: TOGGLE_ON_OFF_MODE, + 1: NON_TOGGLE_ON_MODE, + 2: NON_TOGGLE_OFF_MODE, +} + + +def _bool_schema(name): + return voluptuous_serialize.convert(vol.Schema({vol.Required(name): bool}))[0] + + +def _byte_schema(name): + return voluptuous_serialize.convert(vol.Schema({vol.Required(name): cv.byte}))[0] + + +def _ramp_rate_schema(name): + return voluptuous_serialize.convert( + vol.Schema({vol.Required(name): vol.In(RAMP_RATE_SECONDS)}), + custom_serializer=cv.custom_serializer, + )[0] + + +def get_properties(device: Device): + """Get the properties of an Insteon device and return the records and schema.""" + + properties = [] + schema = {} + + # Limit the properties we manage at this time. + for prop_name in device.operating_flags: + if not device.operating_flags[prop_name].is_read_only: + prop_dict, schema_dict = _get_property(device.operating_flags[prop_name]) + properties.append(prop_dict) + schema[prop_name] = schema_dict + + mask_found = False + for prop_name in device.properties: + if device.properties[prop_name].is_read_only: + continue + + if prop_name == RAMP_RATE: + rr_prop, rr_schema = _get_ramp_rate_property(device.properties[prop_name]) + properties.append(rr_prop) + schema[RAMP_RATE] = rr_schema + + elif not mask_found and "mask" in prop_name: + mask_found = True + toggle_props, toggle_schema = _get_toggle_properties(device) + properties.extend(toggle_props) + schema.update(toggle_schema) + + rb_props, rb_schema = _get_radio_button_properties(device) + properties.extend(rb_props) + schema.update(rb_schema) + else: + prop_dict, schema_dict = _get_property(device.properties[prop_name]) + properties.append(prop_dict) + schema[prop_name] = schema_dict + + return properties, schema + + +def set_property(device, prop_name: str, value): + """Update a property value.""" + if isinstance(value, bool) and prop_name in device.operating_flags: + device.operating_flags[prop_name].new_value = value + + elif prop_name == RAMP_RATE: + device.properties[prop_name].new_value = seconds_to_ramp_rate(value) + + elif prop_name.startswith(RADIO_BUTTON_GROUP_PROP): + buttons = [int(button) for button in value] + rb_groups = _calc_radio_button_groups(device) + curr_group = int(prop_name[len(RADIO_BUTTON_GROUP_PROP) :]) + if len(rb_groups) > curr_group: + removed = [btn for btn in rb_groups[curr_group] if btn not in buttons] + if removed: + device.clear_radio_buttons(removed) + if buttons: + device.set_radio_buttons(buttons) + + elif prop_name.startswith(TOGGLE_PROP): + button_name = prop_name[len(TOGGLE_PROP) :] + for button in device.groups: + if device.groups[button].name == button_name: + device.set_toggle_mode(button, int(value)) + + else: + device.properties[prop_name].new_value = value + + +def _get_property(prop): + """Return a property data row.""" + value, modified = _get_usable_value(prop) + prop_dict = {"name": prop.name, "value": value, "modified": modified} + if isinstance(prop.value, bool): + schema = _bool_schema(prop.name) + else: + schema = _byte_schema(prop.name) + return prop_dict, {"name": prop.name, **schema} + + +def _get_toggle_properties(device): + """Generate the mask properties for a KPL device.""" + props = [] + schema = {} + toggle_prop = device.properties[NON_TOGGLE_MASK] + toggle_on_prop = device.properties[NON_TOGGLE_ON_OFF_MASK] + for button in device.groups: + name = f"{TOGGLE_PROP}{device.groups[button].name}" + value, modified = _toggle_button_value(toggle_prop, toggle_on_prop, button) + props.append({"name": name, "value": value, "modified": modified}) + toggle_schema = vol.Schema({vol.Required(name): vol.In(TOGGLE_MODES_SCHEMA)}) + toggle_schema_dict = voluptuous_serialize.convert( + toggle_schema, custom_serializer=cv.custom_serializer + ) + schema[name] = toggle_schema_dict[0] + return props, schema + + +def _toggle_button_value(non_toggle_prop, toggle_on_prop, button): + """Determine the toggle value of a button.""" + toggle_mask, toggle_modified = _get_usable_value(non_toggle_prop) + toggle_on_mask, toggle_on_modified = _get_usable_value(toggle_on_prop) + + bit = button - 1 + if not toggle_mask & 1 << bit: + value = 0 + else: + if toggle_on_mask & 1 << bit: + value = 1 + else: + value = 2 + + modified = False + if toggle_modified: + curr_bit = non_toggle_prop.value & 1 << bit + new_bit = non_toggle_prop.new_value & 1 << bit + modified = not curr_bit == new_bit + + if not modified and value != 0 and toggle_on_modified: + curr_bit = toggle_on_prop.value & 1 << bit + new_bit = toggle_on_prop.new_value & 1 << bit + modified = not curr_bit == new_bit + + return value, modified + + +def _get_radio_button_properties(device): + """Return the values and schema to set KPL buttons as radio buttons.""" + rb_groups = _calc_radio_button_groups(device) + props = [] + schema = {} + index = 0 + remaining_buttons = [] + + buttons_in_groups = list(chain.from_iterable(rb_groups)) + + # Identify buttons not belonging to any group + for button in device.groups: + if button not in buttons_in_groups: + remaining_buttons.append(button) + + for rb_group in rb_groups: + name = f"{RADIO_BUTTON_GROUP_PROP}{index}" + button_1 = rb_group[0] + button_str = f"_{button_1}" if button_1 != 1 else "" + on_mask = device.properties[f"{ON_MASK}{button_str}"] + off_mask = device.properties[f"{OFF_MASK}{button_str}"] + modified = on_mask.is_dirty or off_mask.is_dirty + + props.append( + { + "name": name, + "modified": modified, + "value": rb_group, + } + ) + + options = { + button: device.groups[button].name + for button in chain.from_iterable([rb_group, remaining_buttons]) + } + rb_schema = vol.Schema({vol.Optional(name): cv.multi_select(options)}) + + rb_schema_dict = voluptuous_serialize.convert( + rb_schema, custom_serializer=cv.custom_serializer + ) + schema[name] = rb_schema_dict[0] + + index += 1 + + if len(remaining_buttons) > 1: + name = f"{RADIO_BUTTON_GROUP_PROP}{index}" + + props.append( + { + "name": name, + "modified": False, + "value": [], + } + ) + + options = {button: device.groups[button].name for button in remaining_buttons} + rb_schema = vol.Schema({vol.Optional(name): cv.multi_select(options)}) + + rb_schema_dict = voluptuous_serialize.convert( + rb_schema, custom_serializer=cv.custom_serializer + ) + schema[name] = rb_schema_dict[0] + + return props, schema + + +def _calc_radio_button_groups(device): + """Return existing radio button groups.""" + rb_groups = [] + for button in device.groups: + if button not in list(chain.from_iterable(rb_groups)): + button_str = "" if button == 1 else f"_{button}" + on_mask, _ = _get_usable_value(device.properties[f"{ON_MASK}{button_str}"]) + if on_mask != 0: + rb_group = [button] + for bit in list(range(0, button - 1)) + list(range(button, 8)): + if on_mask & 1 << bit: + rb_group.append(bit + 1) + if len(rb_group) > 1: + rb_groups.append(rb_group) + return rb_groups + + +def _get_ramp_rate_property(prop): + """Return the value and schema of a ramp rate property.""" + rr_prop, _ = _get_property(prop) + rr_prop["value"] = ramp_rate_to_seconds(rr_prop["value"]) + return rr_prop, _ramp_rate_schema(prop.name) + + +def _get_usable_value(prop): + """Return the current or the modified value of a property.""" + value = prop.value if prop.new_value is None else prop.new_value + return value, prop.is_dirty + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/properties/get", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_properties( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add the default All-Link Database records for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + properties, schema = get_properties(device) + + connection.send_result(msg[ID], {"properties": properties, "schema": schema}) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/properties/change", + vol.Required(DEVICE_ADDRESS): str, + vol.Required(PROPERTY_NAME): str, + vol.Required(PROPERTY_VALUE): vol.Any(list, int, float, bool, str), + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_change_properties_record( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add the default All-Link Database records for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + set_property(device, msg[PROPERTY_NAME], msg[PROPERTY_VALUE]) + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/properties/write", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_write_properties( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add the default All-Link Database records for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + result1 = await device.async_write_op_flags() + result2 = await device.async_write_ext_properties() + await devices.async_save(workdir=hass.config.config_dir) + if result1 != ResponseStatus.SUCCESS or result2 != ResponseStatus.SUCCESS: + connection.send_message( + websocket_api.error_message( + msg[ID], "write_failed", "properties not written to device" + ) + ) + return + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/properties/load", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_load_properties( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add the default All-Link Database records for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + result1 = await device.async_read_op_flags() + result2 = await device.async_read_ext_properties() + await devices.async_save(workdir=hass.config.config_dir) + if result1 != ResponseStatus.SUCCESS or result2 != ResponseStatus.SUCCESS: + connection.send_message( + websocket_api.error_message( + msg[ID], "load_failed", "properties not loaded from device" + ) + ) + return + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/properties/reset", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_reset_properties( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add the default All-Link Database records for an Insteon device.""" + device = devices[msg[DEVICE_ADDRESS]] + if not device: + notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) + return + + for prop in device.operating_flags: + device.operating_flags[prop].new_value = None + for prop in device.properties: + device.properties[prop].new_value = None + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index a40a0b0d4b0..dca53d20369 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -1,4 +1,6 @@ """Constants used by insteon component.""" +import re + from pyinsteon.groups import ( CO_SENSOR, COVER, @@ -158,3 +160,15 @@ STATE_NAME_LABEL_MAP = { COVER: "Cover", RELAY: "Relay", } + +TYPE = "type" +ID = "id" +DEVICE_ID = "device_id" +DEVICE_ADDRESS = "device_address" +ALDB_RECORD = "record" +PROPERTY_NAME = "name" +PROPERTY_VALUE = "value" +HA_DEVICE_NOT_FOUND = "ha_device_not_found" +INSTEON_DEVICE_NOT_FOUND = "insteon_device_not_found" + +INSTEON_ADDR_REGEX = re.compile(r"([A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2})$") diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 4643a8c662a..f5f9d57d8a8 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -10,4 +10,4 @@ ], "config_flow": true, "iot_class": "local_push" -} \ No newline at end of file +} diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 5fb46735f29..626dc7dde4b 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -40,6 +40,7 @@ from .const import ( CONF_X10_ALL_UNITS_OFF, DOMAIN, HOUSECODES, + INSTEON_ADDR_REGEX, PORT_HUB_V1, PORT_HUB_V2, SRV_ALL_LINK_GROUP, @@ -64,6 +65,13 @@ def set_default_port(schema: dict) -> dict: return schema +def insteon_address(value: str) -> str: + """Validate an Insteon address.""" + if not INSTEON_ADDR_REGEX.match(value): + raise vol.Invalid("Invalid Insteon Address") + return str(value).replace(".", "").lower() + + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( vol.Schema( { diff --git a/tests/components/insteon/mock_connection.py b/tests/components/insteon/mock_connection.py new file mode 100644 index 00000000000..00d2c1ec83a --- /dev/null +++ b/tests/components/insteon/mock_connection.py @@ -0,0 +1,11 @@ +"""Mock connections for Insteon.""" + + +async def mock_successful_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def mock_failed_connection(*args, **kwargs): + """Return a failed connection.""" + raise ConnectionError("Connection failed") diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index 7ffb0672161..e28e25bf41b 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -2,11 +2,14 @@ from unittest.mock import AsyncMock, MagicMock from pyinsteon.address import Address +from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( - GeneralController_MiniRemote_4, + DimmableLightingControl_KeypadLinc_8, + GeneralController, Hub, SwitchedLightingControl_SwitchLinc, ) +from pyinsteon.managers.saved_devices_manager import dict_to_aldb_record class MockSwitchLinc(SwitchedLightingControl_SwitchLinc): @@ -32,7 +35,7 @@ class MockDevices: def __getitem__(self, address): """Return a a device from the device address.""" - return self._devices.get(address) + return self._devices.get(Address(address)) def __iter__(self): """Return an iterator of device addresses.""" @@ -53,13 +56,73 @@ class MockDevices: addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") - self._devices[addr0] = Hub(addr0) - self._devices[addr1] = MockSwitchLinc(addr1, 0x02, 0x00) - self._devices[addr2] = GeneralController_MiniRemote_4(addr2, 0x00, 0x00) - self._devices[addr3] = SwitchedLightingControl_SwitchLinc(addr3, 0x02, 0x00) + self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") + self._devices[addr1] = MockSwitchLinc( + addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" + ) + self._devices[addr2] = GeneralController( + addr2, 0x00, 0x00, 0x00, "Device 22.22.22", "2" + ) + self._devices[addr3] = DimmableLightingControl_KeypadLinc_8( + addr3, 0x02, 0x00, 0x00, "Device 33.33.33", "3" + ) + for device in [self._devices[addr] for addr in [addr1, addr2, addr3]]: device.async_read_config = AsyncMock() + device.aldb.async_write = AsyncMock() + device.aldb.async_load = AsyncMock() + device.async_add_default_links = AsyncMock() + device.async_read_op_flags = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + device.async_read_ext_properties = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + device.async_write_op_flags = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + device.async_write_ext_properties = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + for device in [self._devices[addr] for addr in [addr2, addr3]]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) + self._devices[addr0].aldb.async_load = AsyncMock() + + self._devices[addr2].async_read_op_flags = AsyncMock( + return_value=ResponseStatus.FAILURE + ) + self._devices[addr2].async_read_ext_properties = AsyncMock( + return_value=ResponseStatus.FAILURE + ) + self._devices[addr2].async_write_op_flags = AsyncMock( + return_value=ResponseStatus.FAILURE + ) + self._devices[addr2].async_write_ext_properties = AsyncMock( + return_value=ResponseStatus.FAILURE + ) + self.modem = self._devices[addr0] + + def fill_aldb(self, address, records): + """Fill the All-Link Database for a device.""" + device = self._devices[Address(address)] + aldb_records = dict_to_aldb_record(records) + + device.aldb.load_saved_records(ALDBStatus.LOADED, aldb_records) + + def fill_properties(self, address, props_dict): + """Fill the operating flags and extended properties of a device.""" + device = self._devices[Address(address)] + operating_flags = props_dict.get("operating_flags", {}) + properties = props_dict.get("properties", {}) + + for flag in operating_flags: + value = operating_flags[flag] + if device.operating_flags.get(flag): + device.operating_flags[flag].load(value) + for flag in properties: + value = properties[flag] + if device.properties.get(flag): + device.properties[flag].load(value) diff --git a/tests/components/insteon/test_api_aldb.py b/tests/components/insteon/test_api_aldb.py new file mode 100644 index 00000000000..d360b34d7b9 --- /dev/null +++ b/tests/components/insteon/test_api_aldb.py @@ -0,0 +1,288 @@ +"""Test the Insteon All-Link Database APIs.""" + +import json +from unittest.mock import patch + +from pyinsteon import pub +from pyinsteon.address import Address +from pyinsteon.topics import ALDB_STATUS_CHANGED, DEVICE_LINK_CONTROLLER_CREATED +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon.api import async_load_api +from homeassistant.components.insteon.api.aldb import ( + ALDB_RECORD, + DEVICE_ADDRESS, + ID, + TYPE, +) +from homeassistant.components.insteon.api.device import INSTEON_DEVICE_NOT_FOUND + +from .mock_devices import MockDevices + +from tests.common import load_fixture + + +@pytest.fixture(name="aldb_data", scope="session") +def aldb_data_fixture(): + """Load the controller state fixture data.""" + return json.loads(load_fixture("insteon/aldb_data.json")) + + +async def _setup(hass, hass_ws_client, aldb_data): + """Set up tests.""" + ws_client = await hass_ws_client(hass) + devices = MockDevices() + await devices.async_load() + async_load_api(hass) + devices.fill_aldb("33.33.33", aldb_data) + return ws_client, devices + + +def _compare_records(aldb_rec, dict_rec): + """Compare a record in the ALDB to the dictionary record.""" + assert aldb_rec.is_in_use == dict_rec["in_use"] + assert aldb_rec.is_controller == (dict_rec["is_controller"]) + assert not aldb_rec.is_high_water_mark + assert aldb_rec.group == dict_rec["group"] + assert aldb_rec.target == Address(dict_rec["target"]) + assert aldb_rec.data1 == dict_rec["data1"] + assert aldb_rec.data2 == dict_rec["data2"] + assert aldb_rec.data3 == dict_rec["data3"] + + +def _aldb_dict(mem_addr): + """Generate an ALDB record as a dictionary.""" + return { + "mem_addr": mem_addr, + "in_use": True, + "is_controller": True, + "highwater": False, + "group": 100, + "target": "111111", + "data1": 101, + "data2": 102, + "data3": 103, + "dirty": True, + } + + +async def test_get_aldb(hass, hass_ws_client, aldb_data): + """Test getting an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/aldb/get", DEVICE_ADDRESS: "33.33.33"} + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert len(result) == 5 + + +async def test_change_aldb_record(hass, hass_ws_client, aldb_data): + """Test changing an Insteon device's All-Link Database record.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + change_rec = _aldb_dict(4079) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/change", + DEVICE_ADDRESS: "33.33.33", + ALDB_RECORD: change_rec, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(devices["33.33.33"].aldb.pending_changes) == 1 + rec = devices["33.33.33"].aldb.pending_changes[4079] + _compare_records(rec, change_rec) + + +async def test_create_aldb_record(hass, hass_ws_client, aldb_data): + """Test creating a new Insteon All-Link Database record.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + new_rec = _aldb_dict(4079) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/create", + DEVICE_ADDRESS: "33.33.33", + ALDB_RECORD: new_rec, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(devices["33.33.33"].aldb.pending_changes) == 1 + rec = devices["33.33.33"].aldb.pending_changes[-1] + _compare_records(rec, new_rec) + + +async def test_write_aldb(hass, hass_ws_client, aldb_data): + """Test writing an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/write", + DEVICE_ADDRESS: "33.33.33", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].aldb.async_write.call_count == 1 + assert devices["33.33.33"].aldb.async_load.call_count == 1 + assert devices.async_save.call_count == 1 + + +async def test_load_aldb(hass, hass_ws_client, aldb_data): + """Test loading an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/load", + DEVICE_ADDRESS: "AA.AA.AA", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["AA.AA.AA"].aldb.async_load.call_count == 1 + assert devices.async_save.call_count == 1 + + +async def test_reset_aldb(hass, hass_ws_client, aldb_data): + """Test resetting an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + record = _aldb_dict(4079) + devices["33.33.33"].aldb.modify( + mem_addr=record["mem_addr"], + in_use=record["in_use"], + group=record["group"], + controller=record["is_controller"], + target=record["target"], + data1=record["data1"], + data2=record["data2"], + data3=record["data3"], + ) + + assert devices["33.33.33"].aldb.pending_changes + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/reset", + DEVICE_ADDRESS: "33.33.33", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert not devices["33.33.33"].aldb.pending_changes + + +async def test_default_links(hass, hass_ws_client, aldb_data): + """Test getting an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/add_default_links", + DEVICE_ADDRESS: "33.33.33", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].async_add_default_links.call_count == 1 + assert devices["33.33.33"].aldb.async_load.call_count == 1 + assert devices.async_save.call_count == 1 + + +async def test_notify_on_aldb_status(hass, hass_ws_client, aldb_data): + """Test getting an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/notify", + DEVICE_ADDRESS: "33.33.33", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + pub.sendMessage(f"333333.{ALDB_STATUS_CHANGED}") + msg = await ws_client.receive_json() + assert msg["event"]["type"] == "status_changed" + assert not msg["event"]["is_loading"] + + +async def test_notify_on_aldb_record_added(hass, hass_ws_client, aldb_data): + """Test getting an Insteon device's All-Link Database.""" + ws_client, devices = await _setup(hass, hass_ws_client, aldb_data) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/aldb/notify", + DEVICE_ADDRESS: "33.33.33", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + pub.sendMessage( + f"{DEVICE_LINK_CONTROLLER_CREATED}.333333", + controller=Address("11.11.11"), + responder=Address("33.33.33"), + group=100, + ) + msg = await ws_client.receive_json() + assert msg["event"]["type"] == "record_loaded" + + +async def test_bad_address(hass, hass_ws_client, aldb_data): + """Test for a bad Insteon address.""" + ws_client, _ = await _setup(hass, hass_ws_client, aldb_data) + record = _aldb_dict(0) + + ws_id = 0 + for call in ["get", "write", "load", "reset", "add_default_links", "notify"]: + ws_id += 1 + await ws_client.send_json( + { + ID: ws_id, + TYPE: f"insteon/aldb/{call}", + DEVICE_ADDRESS: "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND + + for call in ["change", "create"]: + ws_id += 1 + await ws_client.send_json( + { + ID: ws_id, + TYPE: f"insteon/aldb/{call}", + DEVICE_ADDRESS: "99.99.99", + ALDB_RECORD: record, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND diff --git a/tests/components/insteon/test_api_device.py b/tests/components/insteon/test_api_device.py new file mode 100644 index 00000000000..528d44cc691 --- /dev/null +++ b/tests/components/insteon/test_api_device.py @@ -0,0 +1,139 @@ +"""Test the device level APIs.""" +from unittest.mock import patch + +from homeassistant.components import insteon +from homeassistant.components.insteon.api import async_load_api +from homeassistant.components.insteon.api.device import ( + DEVICE_ID, + HA_DEVICE_NOT_FOUND, + ID, + INSTEON_DEVICE_NOT_FOUND, + TYPE, + async_device_name, +) +from homeassistant.components.insteon.const import DOMAIN +from homeassistant.helpers.device_registry import async_get_registry + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry + + +async def _async_setup(hass, hass_ws_client): + """Set up for tests.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data=MOCK_USER_INPUT_PLM, + options={}, + ) + config_entry.add_to_hass(hass) + async_load_api(hass) + + ws_client = await hass_ws_client(hass) + devices = MockDevices() + await devices.async_load() + + dev_reg = await async_get_registry(hass) + # Create device registry entry for mock node + ha_device = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "11.11.11")}, + name="Device 11.11.11", + ) + return ws_client, devices, ha_device, dev_reg + + +async def test_get_device_api(hass, hass_ws_client): + """Test getting an Insteon device.""" + + ws_client, devices, ha_device, _ = await _async_setup(hass, hass_ws_client) + with patch.object(insteon.api.device, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: ha_device.id} + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["name"] == "Device 11.11.11" + assert result["address"] == "11.11.11" + + +async def test_no_ha_device(hass, hass_ws_client): + """Test response when no HA device exists.""" + + ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + with patch.object(insteon.api.device, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: "not_a_device"} + ) + msg = await ws_client.receive_json() + assert not msg.get("result") + assert msg.get("error") + assert msg["error"]["message"] == HA_DEVICE_NOT_FOUND + + +async def test_no_insteon_device(hass, hass_ws_client): + """Test response when no Insteon device exists.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data=MOCK_USER_INPUT_PLM, + options={}, + ) + config_entry.add_to_hass(hass) + async_load_api(hass) + + ws_client = await hass_ws_client(hass) + devices = MockDevices() + await devices.async_load() + + dev_reg = await async_get_registry(hass) + # Create device registry entry for a Insteon device not in the Insteon devices list + ha_device_1 = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "AA.BB.CC")}, + name="HA Device Only", + ) + # Create device registry entry for a non-Insteon device + ha_device_2 = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("other_domain", "no address")}, + name="HA Device Only", + ) + with patch.object(insteon.api.device, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: ha_device_1.id} + ) + msg = await ws_client.receive_json() + assert not msg.get("result") + assert msg.get("error") + assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND + + await ws_client.send_json( + {ID: 3, TYPE: "insteon/device/get", DEVICE_ID: ha_device_2.id} + ) + msg = await ws_client.receive_json() + assert not msg.get("result") + assert msg.get("error") + assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND + + +async def test_get_ha_device_name(hass, hass_ws_client): + """Test getting the HA device name from an Insteon address.""" + + _, devices, _, device_reg = await _async_setup(hass, hass_ws_client) + + with patch.object(insteon.api.device, "devices", devices): + # Test a real HA and Insteon device + name = await async_device_name(device_reg, "11.11.11") + assert name == "Device 11.11.11" + + # Test no HA device but a real Insteon device + name = await async_device_name(device_reg, "22.22.22") + assert name == "Device 22.22.22 (2)" + + # Test no HA or Insteon device + name = await async_device_name(device_reg, "BB.BB.BB") + assert name == "" diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py new file mode 100644 index 00000000000..9b628f4443a --- /dev/null +++ b/tests/components/insteon/test_api_properties.py @@ -0,0 +1,425 @@ +"""Test the Insteon properties APIs.""" + +import json +from unittest.mock import patch + +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon.api import async_load_api +from homeassistant.components.insteon.api.device import INSTEON_DEVICE_NOT_FOUND +from homeassistant.components.insteon.api.properties import ( + DEVICE_ADDRESS, + ID, + NON_TOGGLE_MASK, + NON_TOGGLE_OFF_MODE, + NON_TOGGLE_ON_MODE, + NON_TOGGLE_ON_OFF_MASK, + PROPERTY_NAME, + PROPERTY_VALUE, + RADIO_BUTTON_GROUP_PROP, + TOGGLE_MODES, + TOGGLE_ON_OFF_MODE, + TOGGLE_PROP, + TYPE, + _get_radio_button_properties, + _get_toggle_properties, +) + +from .mock_devices import MockDevices + +from tests.common import load_fixture + + +@pytest.fixture(name="properties_data", scope="session") +def aldb_data_fixture(): + """Load the controller state fixture data.""" + return json.loads(load_fixture("insteon/kpl_properties.json")) + + +async def _setup(hass, hass_ws_client, properties_data): + """Set up tests.""" + ws_client = await hass_ws_client(hass) + devices = MockDevices() + await devices.async_load() + devices.fill_properties("33.33.33", properties_data) + async_load_api(hass) + return ws_client, devices + + +async def test_get_properties(hass, hass_ws_client, properties_data): + """Test getting an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/properties/get", DEVICE_ADDRESS: "33.33.33"} + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 54 + + +async def test_change_operating_flag(hass, hass_ws_client, properties_data): + """Test changing an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: "led_off", + PROPERTY_VALUE: True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].operating_flags["led_off"].is_dirty + + +async def test_change_property(hass, hass_ws_client, properties_data): + """Test changing an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: "on_mask", + PROPERTY_VALUE: 100, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].properties["on_mask"].new_value == 100 + assert devices["33.33.33"].properties["on_mask"].is_dirty + + +async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data): + """Test changing an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: "ramp_rate", + PROPERTY_VALUE: 4.5, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].properties["ramp_rate"].new_value == 0x1A + assert devices["33.33.33"].properties["ramp_rate"].is_dirty + + +async def test_change_radio_button_group(hass, hass_ws_client, properties_data): + """Test changing an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + rb_props, schema = _get_radio_button_properties(devices["33.33.33"]) + + # Make sure the baseline is correct + assert rb_props[0]["name"] == f"{RADIO_BUTTON_GROUP_PROP}0" + assert rb_props[0]["value"] == [4, 5] + assert rb_props[1]["value"] == [7, 8] + assert rb_props[2]["value"] == [] + assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) + assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) + assert devices["33.33.33"].properties["on_mask"].value == 0 + assert devices["33.33.33"].properties["off_mask"].value == 0 + assert not devices["33.33.33"].properties["on_mask"].is_dirty + assert not devices["33.33.33"].properties["off_mask"].is_dirty + + # Add button 1 to the group + rb_props[0]["value"].append(1) + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}0", + PROPERTY_VALUE: rb_props[0]["value"], + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) + assert 1 in new_rb_props[0]["value"] + assert 4 in new_rb_props[0]["value"] + assert 5 in new_rb_props[0]["value"] + assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) + assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) + + assert devices["33.33.33"].properties["on_mask"].new_value == 0x18 + assert devices["33.33.33"].properties["off_mask"].new_value == 0x18 + assert devices["33.33.33"].properties["on_mask"].is_dirty + assert devices["33.33.33"].properties["off_mask"].is_dirty + + # Remove button 5 + rb_props[0]["value"].remove(5) + await ws_client.send_json( + { + ID: 3, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}0", + PROPERTY_VALUE: rb_props[0]["value"], + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) + assert 1 in new_rb_props[0]["value"] + assert 4 in new_rb_props[0]["value"] + assert 5 not in new_rb_props[0]["value"] + assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) + assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) + + assert devices["33.33.33"].properties["on_mask"].new_value == 0x08 + assert devices["33.33.33"].properties["off_mask"].new_value == 0x08 + assert devices["33.33.33"].properties["on_mask"].is_dirty + assert devices["33.33.33"].properties["off_mask"].is_dirty + + # Remove button group 1 + rb_props[1]["value"] = [] + await ws_client.send_json( + { + ID: 5, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}1", + PROPERTY_VALUE: rb_props[1]["value"], + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) + assert len(new_rb_props) == 2 + assert new_rb_props[0]["value"] == [1, 4] + assert new_rb_props[1]["value"] == [] + + +async def test_create_radio_button_group(hass, hass_ws_client, properties_data): + """Test changing an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) + + # Make sure the baseline is correct + assert len(rb_props) == 3 + print(rb_props) + + rb_props[0]["value"].append("1") + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}2", + PROPERTY_VALUE: ["1", "3"], + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_rb_props, new_schema = _get_radio_button_properties(devices["33.33.33"]) + assert len(new_rb_props) == 4 + assert 1 in new_rb_props[0]["value"] + assert new_schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) + assert not new_schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) + + assert devices["33.33.33"].properties["on_mask"].new_value == 4 + assert devices["33.33.33"].properties["off_mask"].new_value == 4 + assert devices["33.33.33"].properties["on_mask"].is_dirty + assert devices["33.33.33"].properties["off_mask"].is_dirty + + +async def test_change_toggle_property(hass, hass_ws_client, properties_data): + """Update a button's toggle mode.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + device = devices["33.33.33"] + toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) + + # Make sure the baseline is correct + assert toggle_props[0]["name"] == f"{TOGGLE_PROP}{device.groups[1].name}" + assert toggle_props[0]["value"] == TOGGLE_MODES[TOGGLE_ON_OFF_MODE] + assert toggle_props[1]["value"] == TOGGLE_MODES[NON_TOGGLE_ON_MODE] + assert device.properties[NON_TOGGLE_MASK].value == 2 + assert device.properties[NON_TOGGLE_ON_OFF_MASK].value == 2 + assert not device.properties[NON_TOGGLE_MASK].is_dirty + assert not device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: toggle_props[0]["name"], + PROPERTY_VALUE: 1, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) + assert new_toggle_props[0]["value"] == TOGGLE_MODES[NON_TOGGLE_ON_MODE] + assert device.properties[NON_TOGGLE_MASK].new_value == 3 + assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value == 3 + assert device.properties[NON_TOGGLE_MASK].is_dirty + assert device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty + + await ws_client.send_json( + { + ID: 3, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: toggle_props[0]["name"], + PROPERTY_VALUE: 2, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) + assert new_toggle_props[0]["value"] == TOGGLE_MODES[NON_TOGGLE_OFF_MODE] + assert device.properties[NON_TOGGLE_MASK].new_value == 3 + assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value is None + assert device.properties[NON_TOGGLE_MASK].is_dirty + assert not device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty + + await ws_client.send_json( + { + ID: 4, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "33.33.33", + PROPERTY_NAME: toggle_props[1]["name"], + PROPERTY_VALUE: 0, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) + assert new_toggle_props[1]["value"] == TOGGLE_MODES[TOGGLE_ON_OFF_MODE] + assert device.properties[NON_TOGGLE_MASK].new_value == 1 + assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value == 0 + assert device.properties[NON_TOGGLE_MASK].is_dirty + assert device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty + + +async def test_write_properties(hass, hass_ws_client, properties_data): + """Test getting an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/properties/write", DEVICE_ADDRESS: "33.33.33"} + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].async_write_op_flags.call_count == 1 + assert devices["33.33.33"].async_write_ext_properties.call_count == 1 + + +async def test_write_properties_failure(hass, hass_ws_client, properties_data): + """Test getting an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/properties/write", DEVICE_ADDRESS: "22.22.22"} + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "write_failed" + + +async def test_load_properties(hass, hass_ws_client, properties_data): + """Test getting an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert devices["33.33.33"].async_read_op_flags.call_count == 1 + assert devices["33.33.33"].async_read_ext_properties.call_count == 1 + + +async def test_load_properties_failure(hass, hass_ws_client, properties_data): + """Test getting an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "22.22.22"} + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "load_failed" + + +async def test_reset_properties(hass, hass_ws_client, properties_data): + """Test getting an Insteon device's properties.""" + ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + + device = devices["33.33.33"] + device.operating_flags["led_off"].new_value = True + device.properties["on_mask"].new_value = 100 + assert device.operating_flags["led_off"].is_dirty + assert device.properties["on_mask"].is_dirty + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + {ID: 2, TYPE: "insteon/properties/reset", DEVICE_ADDRESS: "33.33.33"} + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert not device.operating_flags["led_off"].is_dirty + assert not device.properties["on_mask"].is_dirty + + +async def test_bad_address(hass, hass_ws_client, properties_data): + """Test for a bad Insteon address.""" + ws_client, _ = await _setup(hass, hass_ws_client, properties_data) + + ws_id = 0 + for call in ["get", "write", "load", "reset"]: + ws_id += 1 + await ws_client.send_json( + { + ID: ws_id, + TYPE: f"insteon/properties/{call}", + DEVICE_ADDRESS: "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND + + ws_id += 1 + await ws_client.send_json( + { + ID: ws_id, + TYPE: "insteon/properties/change", + DEVICE_ADDRESS: "99.99.99", + PROPERTY_NAME: "led_off", + PROPERTY_VALUE: True, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND diff --git a/tests/fixtures/insteon/aldb_data.json b/tests/fixtures/insteon/aldb_data.json new file mode 100644 index 00000000000..2cab1dd5050 --- /dev/null +++ b/tests/fixtures/insteon/aldb_data.json @@ -0,0 +1,67 @@ +{ + "4095": { + "memory": 4095, + "in_use": true, + "controller": false, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 0, + "target": "aaaaaa", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4087": { + "memory": 4087, + "in_use": true, + "controller": true, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 1, + "target": "aaaaaa", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4079": { + "memory": 4079, + "in_use": true, + "controller": false, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 0, + "target": "111111", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4071": { + "memory": 4071, + "in_use": true, + "controller": true, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 2, + "target": "222222", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4063": { + "memory": 4063, + "in_use": true, + "controller": false, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 3, + "target": "333333", + "data1": 0, + "data2": 0, + "data3": 0 + } +} \ No newline at end of file diff --git a/tests/fixtures/insteon/kpl_properties.json b/tests/fixtures/insteon/kpl_properties.json new file mode 100644 index 00000000000..1115428a073 --- /dev/null +++ b/tests/fixtures/insteon/kpl_properties.json @@ -0,0 +1,66 @@ +{ + "operating_flags": { + "program_lock_on": false, + "blink_on_tx_on": false, + "resume_dim_on": false, + "led_on": false, + "key_beep_on": false, + "rf_disable_on": false, + "powerline_disable_on": false, + "blink_on_error_on": false + }, + "properties": { + "led_dimming": 10, + "non_toggle_mask": 2, + "non_toggle_on_off_mask": 2, + "trigger_group_mask": 0, + "on_mask": 0, + "off_mask": 0, + "x10_house": 32, + "x10_unit": 32, + "ramp_rate": 28, + "on_level": 255, + "on_mask_2": 0, + "off_mask_2": 0, + "x10_house_2": 32, + "x10_unit_2": 32, + "ramp_rate_2": 0, + "on_level_2": 0, + "on_mask_3": 0, + "off_mask_3": 0, + "x10_house_3": 32, + "x10_unit_3": 32, + "ramp_rate_3": 0, + "on_level_3": 0, + "on_mask_4": 16, + "off_mask_4": 16, + "x10_house_4": 32, + "x10_unit_4": 32, + "ramp_rate_4": 0, + "on_level_4": 0, + "on_mask_5": 0, + "off_mask_5": 0, + "x10_house_5": 32, + "x10_unit_5": 32, + "ramp_rate_5": 0, + "on_level_5": 0, + "on_mask_6": 0, + "off_mask_6": 0, + "x10_house_6": 32, + "x10_unit_6": 32, + "ramp_rate_6": 0, + "on_level_6": 0, + "on_mask_7": 128, + "off_mask_7": 128, + "x10_house_7": 32, + "x10_unit_7": 32, + "ramp_rate_7": 0, + "on_level_7": 0, + "on_mask_8": 64, + "off_mask_8": 64, + "x10_house_8": 32, + "x10_unit_8": 2, + "ramp_rate_8": 98, + "on_level_8": 74 + } +} \ No newline at end of file From 75f7d3d696659b302627fac277fcf7844414802a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 22 Jul 2021 20:12:33 +0200 Subject: [PATCH 518/818] Replace util.get_local_ip in favor of components.network.async_get_source_ip() - part 1 (#52980) --- .../components/dlna_dmr/manifest.json | 1 + .../components/dlna_dmr/media_player.py | 5 +++-- homeassistant/components/fritz/switch.py | 19 +++++++++++++------ .../components/local_ip/manifest.json | 1 + homeassistant/components/local_ip/sensor.py | 9 ++++++--- homeassistant/components/network/__init__.py | 2 +- homeassistant/components/network/const.py | 2 +- homeassistant/components/upnp/__init__.py | 5 +++-- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/zeroconf/__init__.py | 5 +++-- tests/components/local_ip/test_init.py | 5 +++-- 11 files changed, 36 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index d11b32a6dd5..e9ac437fe46 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,6 +3,7 @@ "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.19.1"], + "dependencies": ["network"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index b2999a5ae56..36f62155b2d 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -24,6 +24,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +from homeassistant.components.network import async_get_source_ip +from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.const import ( CONF_NAME, CONF_URL, @@ -38,7 +40,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.util import get_local_ip import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -142,7 +143,7 @@ async def async_setup_platform( async with hass.data[DLNA_DMR_DATA]["lock"]: server_host = config.get(CONF_LISTEN_IP) if server_host is None: - server_host = get_local_ip() + server_host = await async_get_source_ip(hass, PUBLIC_TARGET_IP) server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT) callback_url_override = config.get(CONF_CALLBACK_URL_OVERRIDE) event_handler = await async_start_event_handler( diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index b1ec63e0ce9..10eb6553dbd 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -16,13 +16,14 @@ from fritzconnection.core.exceptions import ( import slugify as unicode_slug import xmltodict +from homeassistant.components.network import async_get_source_ip from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import get_local_ip, slugify +from homeassistant.util import slugify from .common import ( FritzBoxBaseEntity, @@ -161,7 +162,7 @@ def deflection_entities_list( def port_entities_list( - fritzbox_tools: FritzBoxTools, device_friendly_name: str + fritzbox_tools: FritzBoxTools, device_friendly_name: str, local_ip: str ) -> list[FritzBoxPortSwitch]: """Get list of port forwarding entities.""" @@ -194,7 +195,6 @@ def port_entities_list( port_forwards_count, ) - local_ip = get_local_ip() _LOGGER.debug("IP source for %s is %s", fritzbox_tools.host, local_ip) for i in range(port_forwards_count): @@ -290,12 +290,15 @@ def profile_entities_list( def all_entities_list( - fritzbox_tools: FritzBoxTools, device_friendly_name: str, data_fritz: FritzData + fritzbox_tools: FritzBoxTools, + device_friendly_name: str, + data_fritz: FritzData, + local_ip: str, ) -> list[Entity]: """Get a list of all entities.""" return [ *deflection_entities_list(fritzbox_tools, device_friendly_name), - *port_entities_list(fritzbox_tools, device_friendly_name), + *port_entities_list(fritzbox_tools, device_friendly_name, local_ip), *wifi_entities_list(fritzbox_tools, device_friendly_name), *profile_entities_list(fritzbox_tools, data_fritz), ] @@ -311,8 +314,12 @@ async def async_setup_entry( _LOGGER.debug("Fritzbox services: %s", fritzbox_tools.connection.services) + local_ip = await async_get_source_ip( + fritzbox_tools.hass, target_ip=fritzbox_tools.host + ) + entities_list = await hass.async_add_executor_job( - all_entities_list, fritzbox_tools, entry.title, data_fritz + all_entities_list, fritzbox_tools, entry.title, data_fritz, local_ip ) async_add_entities(entities_list) diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json index f7e245aac05..cec6e094f50 100644 --- a/homeassistant/components/local_ip/manifest.json +++ b/homeassistant/components/local_ip/manifest.json @@ -3,6 +3,7 @@ "name": "Local IP Address", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_ip", + "dependencies": ["network"], "codeowners": ["@issacg"], "iot_class": "local_polling" } diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index c7bc53caa69..661ef88e641 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -1,11 +1,12 @@ """Sensor platform for local_ip.""" +from homeassistant.components.network import async_get_source_ip +from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import get_local_ip from .const import DOMAIN, SENSOR @@ -30,6 +31,8 @@ class IPSensor(SensorEntity): """Initialize the sensor.""" self._attr_name = name - def update(self) -> None: + async def async_update(self) -> None: """Fetch new state data for the sensor.""" - self._attr_state = get_local_ip() + self._attr_state = await async_get_source_ip( + self.hass, target_ip=PUBLIC_TARGET_IP + ) diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 6f11b0947d8..48903d145e7 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -33,7 +33,7 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: @bind_hass -async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str | None: +async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str: """Get the source ip for a target ip.""" adapters = await async_get_adapters(hass) all_ipv4s = [] diff --git a/homeassistant/components/network/const.py b/homeassistant/components/network/const.py index ff69f026fef..8b695a52e13 100644 --- a/homeassistant/components/network/const.py +++ b/homeassistant/components/network/const.py @@ -16,7 +16,7 @@ ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters" DEFAULT_CONFIGURED_ADAPTERS: list[str] = [] MDNS_TARGET_IP: Final = "224.0.0.251" - +PUBLIC_TARGET_IP: Final = "8.8.8.8" NETWORK_CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 5788ec1b3ef..6ad7111ae12 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -6,12 +6,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.components.network import async_get_source_ip +from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType -from homeassistant.util import get_local_ip from .const import ( CONF_LOCAL_IP, @@ -63,7 +64,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): _LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] conf = config.get(DOMAIN, conf_default) - local_ip = await hass.async_add_executor_job(get_local_ip) + local_ip = await async_get_source_ip(hass, PUBLIC_TARGET_IP) hass.data[DOMAIN] = { DOMAIN_CONFIG: conf, DOMAIN_DEVICES: {}, diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 810a53c9e28..41d50b4bae8 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.19.1"], - "dependencies": ["ssdp"], + "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index d8d664b63c5..907ec680cb4 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -19,8 +19,9 @@ from zeroconf import ( ) from zeroconf.asyncio import AsyncServiceInfo -from homeassistant import config_entries, util +from homeassistant import config_entries from homeassistant.components import network +from homeassistant.components.network import async_get_source_ip from homeassistant.components.network.models import Adapter from homeassistant.const import ( EVENT_HOMEASSISTANT_START, @@ -222,7 +223,7 @@ async def _async_register_hass_zc_service( # Set old base URL based on external or internal params["base_url"] = params["external_url"] or params["internal_url"] - host_ip = util.get_local_ip() + host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) try: host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index a7ebfba28e2..be1e689ca16 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -1,6 +1,7 @@ """Tests for the local_ip component.""" from homeassistant.components.local_ip import DOMAIN -from homeassistant.util import get_local_ip +from homeassistant.components.network import async_get_source_ip +from homeassistant.components.zeroconf import MDNS_TARGET_IP from tests.common import MockConfigEntry @@ -13,7 +14,7 @@ async def test_basic_setup(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - local_ip = await hass.async_add_executor_job(get_local_ip) + local_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) state = hass.states.get(f"sensor.{DOMAIN}") assert state assert state.state == local_ip From 0b71055989d7579155200e834ef057efbcf855f3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Jul 2021 12:11:10 -0700 Subject: [PATCH 519/818] Do not automatically add title to strings.json (#53350) --- script/scaffold/generate.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 7ebc364d7ee..122d8570dc1 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -113,7 +113,6 @@ def _custom_tasks(template, info: Info) -> None: elif template == "config_flow": info.update_manifest(config_flow=True) info.update_strings( - title=info.name, config={ "step": { "user": { @@ -138,7 +137,6 @@ def _custom_tasks(template, info: Info) -> None: elif template == "config_flow_discovery": info.update_manifest(config_flow=True) info.update_strings( - title=info.name, config={ "step": { "confirm": { @@ -155,7 +153,6 @@ def _custom_tasks(template, info: Info) -> None: elif template == "config_flow_oauth2": info.update_manifest(config_flow=True, dependencies=["http"]) info.update_strings( - title=info.name, config={ "step": { "pick_implementation": { From 84dc6af7604c355554f1eab4d0d2517bc61b9c16 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 22 Jul 2021 21:56:38 +0200 Subject: [PATCH 520/818] Update to PyVicare 1.0 (#53281) --- homeassistant/components/vicare/__init__.py | 5 +- .../components/vicare/binary_sensor.py | 38 +++--------- homeassistant/components/vicare/climate.py | 58 +++++++++++-------- homeassistant/components/vicare/manifest.json | 2 +- homeassistant/components/vicare/sensor.py | 10 +++- .../components/vicare/water_heater.py | 32 +++++----- requirements_all.txt | 2 +- 7 files changed, 70 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 88c4ce33a86..f3ffd7e1db6 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -9,6 +9,7 @@ from PyViCare.PyViCareHeatPump import HeatPump import voluptuous as vol from homeassistant.const import ( + CONF_CLIENT_ID, CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, @@ -23,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] DOMAIN = "vicare" -PYVICARE_ERROR = "error" VICARE_API = "api" VICARE_NAME = "name" VICARE_HEATING_TYPE = "heating_type" @@ -48,6 +48,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( cv.time_period, lambda value: value.total_seconds() ), @@ -71,7 +72,7 @@ def setup(hass, config): params["circuit"] = conf[CONF_CIRCUIT] params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) - + params["client_id"] = conf.get(CONF_CLIENT_ID) heating_type = conf[CONF_HEATING_TYPE] try: diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 823c4f1ba1b..0c98d22e9ae 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -1,6 +1,8 @@ """Viessmann ViCare sensor device.""" +from contextlib import suppress import logging +from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError import requests from homeassistant.components.binary_sensor import ( @@ -11,7 +13,6 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from . import ( DOMAIN as VICARE_DOMAIN, - PYVICARE_ERROR, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME, @@ -29,10 +30,6 @@ SENSOR_BURNER_ACTIVE = "burner_active" # heatpump sensors SENSOR_COMPRESSOR_ACTIVE = "compressor_active" -SENSOR_HEATINGROD_OVERALL = "heatingrod_overall" -SENSOR_HEATINGROD_LEVEL1 = "heatingrod_level1" -SENSOR_HEATINGROD_LEVEL2 = "heatingrod_level2" -SENSOR_HEATINGROD_LEVEL3 = "heatingrod_level3" SENSOR_TYPES = { SENSOR_CIRCULATION_PUMP_ACTIVE: { @@ -52,26 +49,6 @@ SENSOR_TYPES = { CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, CONF_GETTER: lambda api: api.getCompressorActive(), }, - SENSOR_HEATINGROD_OVERALL: { - CONF_NAME: "Heating rod overall", - CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, - CONF_GETTER: lambda api: api.getHeatingRodStatusOverall(), - }, - SENSOR_HEATINGROD_LEVEL1: { - CONF_NAME: "Heating rod level 1", - CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, - CONF_GETTER: lambda api: api.getHeatingRodStatusLevel1(), - }, - SENSOR_HEATINGROD_LEVEL2: { - CONF_NAME: "Heating rod level 2", - CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, - CONF_GETTER: lambda api: api.getHeatingRodStatusLevel2(), - }, - SENSOR_HEATINGROD_LEVEL3: { - CONF_NAME: "Heating rod level 3", - CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, - CONF_GETTER: lambda api: api.getHeatingRodStatusLevel3(), - }, } SENSORS_GENERIC = [SENSOR_CIRCULATION_PUMP_ACTIVE] @@ -80,10 +57,6 @@ SENSORS_BY_HEATINGTYPE = { HeatingType.gas: [SENSOR_BURNER_ACTIVE], HeatingType.heatpump: [ SENSOR_COMPRESSOR_ACTIVE, - SENSOR_HEATINGROD_OVERALL, - SENSOR_HEATINGROD_LEVEL1, - SENSOR_HEATINGROD_LEVEL2, - SENSOR_HEATINGROD_LEVEL3, ], HeatingType.fuelcell: [SENSOR_BURNER_ACTIVE], } @@ -126,7 +99,7 @@ class ViCareBinarySensor(BinarySensorEntity): @property def available(self): """Return True if entity is available.""" - return self._state is not None and self._state != PYVICARE_ERROR + return self._state is not None @property def unique_id(self): @@ -151,8 +124,11 @@ class ViCareBinarySensor(BinarySensorEntity): def update(self): """Update state of sensor.""" try: - self._state = self._sensor[CONF_GETTER](self._api) + with suppress(PyViCareNotSupportedFeatureError): + self._state = self._sensor[CONF_GETTER](self._api) except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except ValueError: _LOGGER.error("Unable to decode data from ViCare server") + except PyViCareRateLimitError as limit_exception: + _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index cfbfa1ddec6..2822d048152 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,6 +1,8 @@ """Viessmann ViCare climate device.""" +from contextlib import suppress import logging +from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError import requests import voluptuous as vol @@ -21,7 +23,6 @@ from homeassistant.helpers import entity_platform from . import ( DOMAIN as VICARE_DOMAIN, - PYVICARE_ERROR, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME, @@ -136,47 +137,58 @@ class ViCareClimate(ClimateEntity): def update(self): """Let HA know there has been an update from the ViCare API.""" try: - _room_temperature = self._api.getRoomTemperature() - _supply_temperature = self._api.getSupplyTemperature() - if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: + _room_temperature = None + with suppress(PyViCareNotSupportedFeatureError): + _room_temperature = self._api.getRoomTemperature() + + _supply_temperature = None + with suppress(PyViCareNotSupportedFeatureError): + _supply_temperature = self._api.getSupplyTemperature() + + if _room_temperature is not None: self._current_temperature = _room_temperature - elif _supply_temperature != PYVICARE_ERROR: + elif _supply_temperature is not None: self._current_temperature = _supply_temperature else: self._current_temperature = None - self._current_program = self._api.getActiveProgram() - # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby - desired_temperature = self._api.getCurrentDesiredTemperature() - if desired_temperature == PYVICARE_ERROR: - desired_temperature = None + with suppress(PyViCareNotSupportedFeatureError): + self._current_program = self._api.getActiveProgram() - self._target_temperature = desired_temperature + with suppress(PyViCareNotSupportedFeatureError): + self._target_temperature = self._api.getCurrentDesiredTemperature() - self._current_mode = self._api.getActiveMode() + with suppress(PyViCareNotSupportedFeatureError): + self._current_mode = self._api.getActiveMode() # Update the generic device attributes self._attributes = {} + self._attributes["room_temperature"] = _room_temperature self._attributes["active_vicare_program"] = self._current_program self._attributes["active_vicare_mode"] = self._current_mode - self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() - self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() - self._attributes[ - "month_since_last_service" - ] = self._api.getMonthSinceLastService() - self._attributes["date_last_service"] = self._api.getLastServiceDate() - self._attributes["error_history"] = self._api.getErrorHistory() - self._attributes["active_error"] = self._api.getActiveError() + + with suppress(PyViCareNotSupportedFeatureError): + self._attributes[ + "heating_curve_slope" + ] = self._api.getHeatingCurveSlope() + + with suppress(PyViCareNotSupportedFeatureError): + self._attributes[ + "heating_curve_shift" + ] = self._api.getHeatingCurveShift() # Update the specific device attributes if self._heating_type == HeatingType.gas: - self._current_action = self._api.getBurnerActive() - + with suppress(PyViCareNotSupportedFeatureError): + self._current_action = self._api.getBurnerActive() elif self._heating_type == HeatingType.heatpump: - self._current_action = self._api.getCompressorActive() + with suppress(PyViCareNotSupportedFeatureError): + self._current_action = self._api.getCompressorActive() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") + except PyViCareRateLimitError as limit_exception: + _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) except ValueError: _LOGGER.error("Unable to decode data from ViCare server") diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 400618c3e85..88e9a1e4e4b 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,6 +3,6 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.2.5"], + "requirements": ["PyViCare==1.0.0"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 7d224de3835..4f7ab9df985 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -1,6 +1,8 @@ """Viessmann ViCare sensor device.""" +from contextlib import suppress import logging +from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError import requests from homeassistant.components.sensor import SensorEntity @@ -21,7 +23,6 @@ from homeassistant.const import ( from . import ( DOMAIN as VICARE_DOMAIN, - PYVICARE_ERROR, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME, @@ -350,7 +351,7 @@ class ViCareSensor(SensorEntity): @property def available(self): """Return True if entity is available.""" - return self._state is not None and self._state != PYVICARE_ERROR + return self._state is not None @property def unique_id(self): @@ -385,8 +386,11 @@ class ViCareSensor(SensorEntity): def update(self): """Update state of sensor.""" try: - self._state = self._sensor[CONF_GETTER](self._api) + with suppress(PyViCareNotSupportedFeatureError): + self._state = self._sensor[CONF_GETTER](self._api) except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except ValueError: _LOGGER.error("Unable to decode data from ViCare server") + except PyViCareRateLimitError as limit_exception: + _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index cbecf7fdaf2..af373c6ee6e 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,6 +1,8 @@ """Viessmann ViCare water_heater device.""" +from contextlib import suppress import logging +from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError import requests from homeassistant.components.water_heater import ( @@ -9,13 +11,7 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import ( - DOMAIN as VICARE_DOMAIN, - PYVICARE_ERROR, - VICARE_API, - VICARE_HEATING_TYPE, - VICARE_NAME, -) +from . import DOMAIN as VICARE_DOMAIN, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME _LOGGER = logging.getLogger(__name__) @@ -81,19 +77,23 @@ class ViCareWater(WaterHeaterEntity): def update(self): """Let HA know there has been an update from the ViCare API.""" try: - current_temperature = self._api.getDomesticHotWaterStorageTemperature() - if current_temperature != PYVICARE_ERROR: - self._current_temperature = current_temperature - else: - self._current_temperature = None + with suppress(PyViCareNotSupportedFeatureError): + self._current_temperature = ( + self._api.getDomesticHotWaterStorageTemperature() + ) - self._target_temperature = ( - self._api.getDomesticHotWaterConfiguredTemperature() - ) + with suppress(PyViCareNotSupportedFeatureError): + self._target_temperature = ( + self._api.getDomesticHotWaterConfiguredTemperature() + ) + + with suppress(PyViCareNotSupportedFeatureError): + self._current_mode = self._api.getActiveMode() - self._current_mode = self._api.getActiveMode() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") + except PyViCareRateLimitError as limit_exception: + _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) except ValueError: _LOGGER.error("Unable to decode data from ViCare server") diff --git a/requirements_all.txt b/requirements_all.txt index 9c9e88f22ae..f6ba5dc03df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,7 +61,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.5.0 # homeassistant.components.vicare -PyViCare==0.2.5 +PyViCare==1.0.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 12503d548b78f2b93745c6a396a0d0e39cb18ea3 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 22 Jul 2021 16:40:32 -0400 Subject: [PATCH 521/818] Use entity class attributes for canary (#53333) Co-authored-by: Franck Nijhof --- .../components/canary/alarm_control_panel.py | 21 ++----- homeassistant/components/canary/camera.py | 36 +++--------- homeassistant/components/canary/sensor.py | 56 ++++--------------- 3 files changed, 25 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 4e29c40f49f..4d29d4893e7 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -52,6 +52,9 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): """Representation of a Canary alarm control panel.""" coordinator: CanaryDataUpdateCoordinator + _attr_supported_features = ( + SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + ) def __init__( self, coordinator: CanaryDataUpdateCoordinator, location: Location @@ -59,23 +62,14 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): """Initialize a Canary security camera.""" super().__init__(coordinator) self._location_id: str = location.location_id - self._location_name: str = location.name + self._attr_name = location.name + self._attr_unique_id = str(self._location_id) @property def location(self) -> Location: """Return information about the location.""" return self.coordinator.data["locations"][self._location_id] - @property - def name(self) -> str: - """Return the name of the alarm.""" - return self._location_name - - @property - def unique_id(self) -> str: - """Return the unique ID of the alarm.""" - return str(self._location_id) - @property def state(self) -> str | None: """Return the state of the device.""" @@ -92,11 +86,6 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): return None - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index b1725945db2..2699ba1f640 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -30,7 +29,6 @@ from .const import ( CONF_FFMPEG_ARGUMENTS, DATA_COORDINATOR, DEFAULT_FFMPEG_ARGUMENTS, - DEFAULT_TIMEOUT, DOMAIN, MANUFACTURER, ) @@ -73,7 +71,6 @@ async def async_setup_entry( coordinator, location_id, device, - DEFAULT_TIMEOUT, ffmpeg_arguments, ) ) @@ -92,7 +89,6 @@ class CanaryCamera(CoordinatorEntity, Camera): coordinator: CanaryDataUpdateCoordinator, location_id: str, device: Device, - timeout: int, ffmpeg_args: str, ) -> None: """Initialize a Canary security camera.""" @@ -102,37 +98,21 @@ class CanaryCamera(CoordinatorEntity, Camera): self._ffmpeg_arguments = ffmpeg_args self._location_id = location_id self._device = device - self._device_id: str = device.device_id - self._device_name: str = device.name - self._device_type_name = device.device_type["name"] - self._timeout = timeout self._live_stream_session: LiveStreamSession | None = None + self._attr_name = device.name + self._attr_unique_id = str(device.device_id) + self._attr_device_info = { + "identifiers": {(DOMAIN, str(device.device_id))}, + "name": device.name, + "model": device.device_type["name"], + "manufacturer": MANUFACTURER, + } @property def location(self) -> Location: """Return information about the location.""" return self.coordinator.data["locations"][self._location_id] - @property - def name(self) -> str: - """Return the name of this device.""" - return self._device_name - - @property - def unique_id(self) -> str: - """Return the unique ID of this camera.""" - return str(self._device_id) - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, str(self._device_id))}, - "name": self._device_name, - "model": self._device_type_name, - "manufacturer": MANUFACTURER, - } - @property def is_recording(self) -> bool: """Return true if the device is recording.""" diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 91dc3bad5eb..5c92f0089f2 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -17,7 +17,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -97,11 +96,9 @@ class CanarySensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self._sensor_type = sensor_type self._device_id = device.device_id - self._device_name = device.name - self._device_type_name = device.device_type["name"] sensor_type_name = sensor_type[0].replace("_", " ").title() - self._name = f"{location.name} {device.name} {sensor_type_name}" + self._attr_name = f"{location.name} {device.name} {sensor_type_name}" canary_sensor_type = None if self._sensor_type[0] == "air_quality": @@ -116,6 +113,17 @@ class CanarySensor(CoordinatorEntity, SensorEntity): canary_sensor_type = SensorType.BATTERY self._canary_type = canary_sensor_type + self._attr_state = self.reading + self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}" + self._attr_device_info = { + "identifiers": {(DOMAIN, str(device.device_id))}, + "name": device.name, + "model": device.device_type["name"], + "manufacturer": MANUFACTURER, + } + self._attr_unit_of_measurement = sensor_type[1] + self._attr_device_class = sensor_type[3] + self._attr_icon = sensor_type[2] @property def reading(self) -> float | None: @@ -136,46 +144,6 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None - @property - def name(self) -> str: - """Return the name of the Canary sensor.""" - return self._name - - @property - def state(self) -> float | None: - """Return the state of the sensor.""" - return self.reading - - @property - def unique_id(self) -> str: - """Return the unique ID of this sensor.""" - return f"{self._device_id}_{self._sensor_type[0]}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, str(self._device_id))}, - "name": self._device_name, - "model": self._device_type_name, - "manufacturer": MANUFACTURER, - } - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self._sensor_type[1] - - @property - def device_class(self) -> str | None: - """Device class for the sensor.""" - return self._sensor_type[3] - - @property - def icon(self) -> str | None: - """Icon for the sensor.""" - return self._sensor_type[2] - @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes.""" From c875ff8648c5bfbcdf9f57d1f1ccf8c7384a2d51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Jul 2021 00:05:59 +0200 Subject: [PATCH 522/818] Store JSON in database in compact format (#53364) * Store JSON in database in compact format * Fix logbook --- homeassistant/components/logbook/__init__.py | 9 ++++----- homeassistant/components/recorder/models.py | 7 +++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 0a367907464..8992ca2d7fc 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -48,11 +48,10 @@ from homeassistant.helpers.integration_platform import ( from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -ENTITY_ID_JSON_TEMPLATE = '"entity_id": "{}"' -ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": "([^"]+)"') -DOMAIN_JSON_EXTRACT = re.compile('"domain": "([^"]+)"') -ICON_JSON_EXTRACT = re.compile('"icon": "([^"]+)"') - +ENTITY_ID_JSON_TEMPLATE = '"entity_id": ?"{}"' +ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') +DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') +ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') ATTR_MESSAGE = "message" CONTINUOUS_DOMAINS = ["proximity", "sensor"] diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 4fd51886246..929115bdf25 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -101,7 +101,8 @@ class Events(Base): # type: ignore """Create an event database object from a native event.""" return Events( event_type=event.event_type, - event_data=event_data or json.dumps(event.data, cls=JSONEncoder), + event_data=event_data + or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")), origin=str(event.origin.value), time_fired=event.time_fired, context_id=event.context.id, @@ -184,7 +185,9 @@ class States(Base): # type: ignore else: dbstate.domain = state.domain dbstate.state = state.state - dbstate.attributes = json.dumps(dict(state.attributes), cls=JSONEncoder) + dbstate.attributes = json.dumps( + dict(state.attributes), cls=JSONEncoder, separators=(",", ":") + ) dbstate.last_changed = state.last_changed dbstate.last_updated = state.last_updated From bfdbb93d2d23b539ebff24dccc3e4221c8bcd0c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Jul 2021 12:06:37 -1000 Subject: [PATCH 523/818] Bump HAP-python to 3.5.2 (#53362) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 40bca03bae0..5ec935611f7 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.5.1", + "HAP-python==3.5.2", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index f6ba5dc03df..bf342ef9cca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.5.1 +HAP-python==3.5.2 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c003da1711c..787d30704ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.5.1 +HAP-python==3.5.2 # homeassistant.components.flick_electric PyFlick==0.0.2 From e85b0ec052d70ba9217a70cfb306b5dd5997ccd8 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 23 Jul 2021 00:40:30 +0200 Subject: [PATCH 524/818] Move Sonos to upstream SoCo (#53351) --- homeassistant/components/sonos/__init__.py | 14 +++++++------- homeassistant/components/sonos/alarms.py | 6 +++--- homeassistant/components/sonos/config_flow.py | 4 ++-- homeassistant/components/sonos/entity.py | 4 ++-- homeassistant/components/sonos/favorites.py | 6 +++--- homeassistant/components/sonos/helpers.py | 2 +- .../components/sonos/household_coordinator.py | 2 +- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/media_player.py | 4 ++-- homeassistant/components/sonos/speaker.py | 14 +++++++------- homeassistant/components/sonos/switch.py | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- tests/components/sonos/conftest.py | 6 +++--- tests/components/sonos/test_config_flow.py | 2 +- tests/components/sonos/test_init.py | 6 +++--- tests/components/sonos/test_sensor.py | 2 +- 17 files changed, 44 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 26541db5fd8..b7f17752097 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -9,10 +9,10 @@ import logging import socket from urllib.parse import urlparse -import pysonos -from pysonos import events_asyncio -from pysonos.core import SoCo -from pysonos.exceptions import SoCoException +from soco import events_asyncio +import soco.config as soco_config +from soco.core import SoCo +from soco.exceptions import SoCoException import voluptuous as vol from homeassistant import config_entries @@ -109,7 +109,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" - pysonos.config.EVENTS_MODULE = events_asyncio + soco_config.EVENTS_MODULE = events_asyncio if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: advertise_addr = config.get(CONF_ADVERTISE_ADDR) if advertise_addr: - pysonos.config.EVENT_ADVERTISE_IP = advertise_addr + soco_config.EVENT_ADVERTISE_IP = advertise_addr if deprecated_address := config.get(CONF_INTERFACE_ADDR): _LOGGER.warning( @@ -140,7 +140,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None: """Create a soco instance and return if successful.""" try: - soco = pysonos.SoCo(ip_address) + soco = SoCo(ip_address) # Ensure that the player is available and UID is cached _ = soco.uid _ = soco.volume diff --git a/homeassistant/components/sonos/alarms.py b/homeassistant/components/sonos/alarms.py index 98e4b752cad..215e4fede32 100644 --- a/homeassistant/components/sonos/alarms.py +++ b/homeassistant/components/sonos/alarms.py @@ -5,9 +5,9 @@ from collections.abc import Iterator import logging from typing import Any -from pysonos import SoCo -from pysonos.alarms import Alarm, get_alarms -from pysonos.exceptions import SoCoException +from soco import SoCo +from soco.alarms import Alarm, get_alarms +from soco.exceptions import SoCoException from homeassistant.helpers.dispatcher import async_dispatcher_send diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 9ee571471bd..762dbc5f0ee 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,7 +1,7 @@ """Config flow for SONOS.""" import logging -import pysonos +import soco from homeassistant import config_entries from homeassistant.const import CONF_HOST @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - result = await hass.async_add_executor_job(pysonos.discover) + result = await hass.async_add_executor_job(soco.discover) return bool(result) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index a2b0d7c5a64..b670ab96f2f 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -4,8 +4,8 @@ from __future__ import annotations import datetime import logging -from pysonos.core import SoCo -from pysonos.exceptions import SoCoException +from soco.core import SoCo +from soco.exceptions import SoCoException import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 25fc58ebba2..9695265b24d 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -5,9 +5,9 @@ from collections.abc import Iterator import logging from typing import Any -from pysonos import SoCo -from pysonos.data_structures import DidlFavorite -from pysonos.exceptions import SoCoException +from soco import SoCo +from soco.data_structures import DidlFavorite +from soco.exceptions import SoCoException from homeassistant.helpers.dispatcher import async_dispatcher_send diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 675a3e8e9f2..0854361cd79 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -5,7 +5,7 @@ import functools as ft import logging from typing import Any, Callable -from pysonos.exceptions import SoCoException, SoCoUPnPException +from soco.exceptions import SoCoException, SoCoUPnPException from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index d24ab40b3db..da964e93984 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine import logging from typing import Any -from pysonos import SoCo +from soco import SoCo from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index a35faee4ad6..e571693c659 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.53"], + "requirements": ["soco==0.23.1"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index a4cc6e175ec..ecd22c89a87 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -6,8 +6,8 @@ import logging from typing import Any import urllib.parse -from pysonos import alarms -from pysonos.core import ( +from soco import alarms +from soco.core import ( MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, PLAY_MODE_BY_MEANING, diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index d7bc1269ea1..af697836908 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -11,13 +11,13 @@ from typing import Any, Callable import urllib.parse import async_timeout -from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo -from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer -from pysonos.events_base import Event as SonosEvent, SubscriptionBase -from pysonos.exceptions import SoCoException, SoCoUPnPException -from pysonos.music_library import MusicLibrary -from pysonos.plugins.sharelink import ShareLinkPlugin -from pysonos.snapshot import Snapshot +from soco.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo +from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer +from soco.events_base import Event as SonosEvent, SubscriptionBase +from soco.exceptions import SoCoException, SoCoUPnPException +from soco.music_library import MusicLibrary +from soco.plugins.sharelink import ShareLinkPlugin +from soco.snapshot import Snapshot from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 795eded6ec1..482780453af 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime import logging -from pysonos.exceptions import SoCoException, SoCoUPnPException +from soco.exceptions import SoCoException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ATTR_TIME diff --git a/requirements_all.txt b/requirements_all.txt index bf342ef9cca..021a20daea2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1779,9 +1779,6 @@ pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 -# homeassistant.components.sonos -pysonos==0.0.53 - # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -2146,6 +2143,9 @@ smhi-pkg==1.0.15 # homeassistant.components.snapcast snapcast==2.1.3 +# homeassistant.components.sonos +soco==0.23.1 + # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 787d30704ef..d27ea0a42b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1015,9 +1015,6 @@ pysmartthings==0.7.6 # homeassistant.components.soma pysoma==0.0.10 -# homeassistant.components.sonos -pysonos==0.0.53 - # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1175,6 +1172,9 @@ smarthab==0.21 # homeassistant.components.smhi smhi-pkg==1.0.15 +# homeassistant.components.sonos +soco==0.23.1 + # homeassistant.components.solaredge solaredge==0.0.2 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 7c5b4ac91ef..294e243901a 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -49,8 +49,8 @@ def config_entry_fixture(): @pytest.fixture(name="soco") def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): - """Create a mock pysonos SoCo fixture.""" - with patch("pysonos.SoCo", autospec=True) as mock, patch( + """Create a mock soco SoCo fixture.""" + with patch("homeassistant.components.sonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" ): mock_soco = mock.return_value @@ -76,7 +76,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): @pytest.fixture(name="discover", autouse=True) def discover_fixture(soco): - """Create a mock pysonos discover fixture.""" + """Create a mock soco discover fixture.""" def do_callback(hass, callback, *args, **kwargs): callback( diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 9dd308ae28f..90ffdb155ea 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, core, setup from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN -@patch("homeassistant.components.sonos.config_flow.pysonos.discover", return_value=True) +@patch("homeassistant.components.sonos.config_flow.soco.discover", return_value=True) async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): """Test we get the user initiated form.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 86ec90f32b8..bf4b5d5e7cc 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -13,7 +13,7 @@ async def test_creating_entry_sets_up_media_player(hass): with patch( "homeassistant.components.sonos.media_player.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pysonos.discover", return_value=True): + ) as mock_setup, patch("soco.discover", return_value=True): result = await hass.config_entries.flow.async_init( sonos.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -33,7 +33,7 @@ async def test_configuring_sonos_creates_entry(hass): """Test that specifying config will create an entry.""" with patch( "homeassistant.components.sonos.async_setup_entry", return_value=mock_coro(True) - ) as mock_setup, patch("pysonos.discover", return_value=True): + ) as mock_setup, patch("soco.discover", return_value=True): await async_setup_component( hass, sonos.DOMAIN, @@ -48,7 +48,7 @@ async def test_not_configuring_sonos_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch( "homeassistant.components.sonos.async_setup_entry", return_value=mock_coro(True) - ) as mock_setup, patch("pysonos.discover", return_value=True): + ) as mock_setup, patch("soco.discover", return_value=True): await async_setup_component(hass, sonos.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 80f050fe6fc..18cd87ca9be 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -1,5 +1,5 @@ """Tests for the Sonos battery sensor platform.""" -from pysonos.exceptions import NotSupportedException +from soco.exceptions import NotSupportedException from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE From 969be5c5396b18319082dcc389ae684988c532a1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 23 Jul 2021 00:12:05 +0000 Subject: [PATCH 525/818] [ci skip] Translation update --- .../components/adax/translations/fr.json | 14 +++++++++ .../components/adax/translations/zh-Hant.json | 20 +++++++++++++ .../airvisual/translations/sensor.fr.json | 11 +++++++ .../components/automate/translations/ca.json | 19 ++++++++++++ .../components/automate/translations/et.json | 19 ++++++++++++ .../components/automate/translations/ru.json | 19 ++++++++++++ .../components/co2signal/translations/fr.json | 30 +++++++++++++++++++ .../components/flipr/translations/de.json | 30 +++++++++++++++++++ .../components/flipr/translations/fr.json | 29 ++++++++++++++++++ .../components/flipr/translations/ru.json | 30 +++++++++++++++++++ .../flipr/translations/zh-Hant.json | 30 +++++++++++++++++++ .../growatt_server/translations/zh-Hant.json | 1 + .../components/homekit/translations/ca.json | 2 +- .../components/homekit/translations/et.json | 2 +- .../homekit/translations/zh-Hant.json | 2 +- .../components/honeywell/translations/fr.json | 11 +++++++ .../nfandroidtv/translations/zh-Hant.json | 21 +++++++++++++ .../speedtestdotnet/translations/en.json | 3 +- .../switcher_kis/translations/fr.json | 12 ++++++++ .../synology_dsm/translations/de.json | 11 ++++++- .../synology_dsm/translations/fr.json | 7 +++++ .../synology_dsm/translations/zh-Hant.json | 11 ++++++- .../components/zwave_js/translations/fr.json | 7 +++++ 23 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/adax/translations/fr.json create mode 100644 homeassistant/components/adax/translations/zh-Hant.json create mode 100644 homeassistant/components/airvisual/translations/sensor.fr.json create mode 100644 homeassistant/components/automate/translations/ca.json create mode 100644 homeassistant/components/automate/translations/et.json create mode 100644 homeassistant/components/automate/translations/ru.json create mode 100644 homeassistant/components/co2signal/translations/fr.json create mode 100644 homeassistant/components/flipr/translations/de.json create mode 100644 homeassistant/components/flipr/translations/fr.json create mode 100644 homeassistant/components/flipr/translations/ru.json create mode 100644 homeassistant/components/flipr/translations/zh-Hant.json create mode 100644 homeassistant/components/honeywell/translations/fr.json create mode 100644 homeassistant/components/nfandroidtv/translations/zh-Hant.json create mode 100644 homeassistant/components/switcher_kis/translations/fr.json diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json new file mode 100644 index 00000000000..a8f036f6ed5 --- /dev/null +++ b/homeassistant/components/adax/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json new file mode 100644 index 00000000000..9685227f617 --- /dev/null +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "account_id": "\u5e33\u865f ID", + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.fr.json b/homeassistant/components/airvisual/translations/sensor.fr.json new file mode 100644 index 00000000000..b3018d53bc2 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.fr.json @@ -0,0 +1,11 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Monoxyde de carbone" + }, + "airvisual__pollutant_level": { + "good": "Bon", + "hazardous": "Hasardeux" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/ca.json b/homeassistant/components/automate/translations/ca.json new file mode 100644 index 00000000000..69cd7de20aa --- /dev/null +++ b/homeassistant/components/automate/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/et.json b/homeassistant/components/automate/translations/et.json new file mode 100644 index 00000000000..da2c8eef4d2 --- /dev/null +++ b/homeassistant/components/automate/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/ru.json b/homeassistant/components/automate/translations/ru.json new file mode 100644 index 00000000000..1212d054ff7 --- /dev/null +++ b/homeassistant/components/automate/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/fr.json b/homeassistant/components/co2signal/translations/fr.json new file mode 100644 index 00000000000..549124674dd --- /dev/null +++ b/homeassistant/components/co2signal/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "unknown": "Erreur inattendue" + }, + "error": { + "api_ratelimit": "Limite de d\u00e9bit API d\u00e9pass\u00e9e", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "country": { + "data": { + "country_code": "Code pays" + } + }, + "user": { + "data": { + "api_key": "Token d'acc\u00e8s" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/de.json b/homeassistant/components/flipr/translations/de.json new file mode 100644 index 00000000000..4dbbfbc9ef9 --- /dev/null +++ b/homeassistant/components/flipr/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_flipr_id_found": "Deinem Konto ist im Moment keine Flipr-ID zugeordnet. Du solltest zuerst \u00fcberpr\u00fcfen, ob es mit der mobilen App von Flipr funktioniert.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr-ID" + }, + "description": "W\u00e4hle deine Flipr-ID in der Liste", + "title": "W\u00e4hle deinen Flipr" + }, + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + }, + "description": "Verbinde dich mit deinem Flipr-Konto.", + "title": "Mit Flipr verbinden" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/fr.json b/homeassistant/components/flipr/translations/fr.json new file mode 100644 index 00000000000..32769aab9b7 --- /dev/null +++ b/homeassistant/components/flipr/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification invalide", + "no_flipr_id_found": "Aucun identifiant Flipr n'est associ\u00e9 \u00e0 votre compte pour le moment. Vous devez d'abord v\u00e9rifier qu'il fonctionne avec l'application mobile de Flipr.", + "unknown": "Erreur inattendue" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Choisissez votre ID Flipr dans la liste", + "title": "Choisir votre Flipr" + }, + "user": { + "data": { + "email": "Email", + "password": "Mot de passe" + }, + "description": "Connectez-vous \u00e0 votre compte Flipr.", + "title": "Connexion a Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/ru.json b/homeassistant/components/flipr/translations/ru.json new file mode 100644 index 00000000000..d7625b5bb41 --- /dev/null +++ b/homeassistant/components/flipr/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_flipr_id_found": "\u041d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0441 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e \u043d\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u044b Flipr ID. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 Flipr.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 Flipr ID \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0412\u0430\u0448 Flipr" + }, + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u0441\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Flipr.", + "title": "Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/zh-Hant.json b/homeassistant/components/flipr/translations/zh-Hant.json new file mode 100644 index 00000000000..546db0beccf --- /dev/null +++ b/homeassistant/components/flipr/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_flipr_id_found": "\u76ee\u524d\u5e33\u865f\u4e2d\u6c92\u6709\u4efb\u4f55\u95dc\u806f\u7684 Flipr ID\uff0c\u8acb\u5148\u900f\u904e Flipr \u624b\u6a5f App \u9032\u884c\u9a57\u8b49\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "\u7531\u5217\u8868\u4e2d\u9078\u64c7 Flipr ID", + "title": "\u9078\u64c7 Flipr ID" + }, + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "description": "\u4f7f\u7528 Flipr \u5e33\u865f\u9032\u884c\u9023\u7dda\u3002", + "title": "\u9023\u7dda\u81f3 Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/zh-Hant.json b/homeassistant/components/growatt_server/translations/zh-Hant.json index 4d00b4e8066..62991306cb8 100644 --- a/homeassistant/components/growatt_server/translations/zh-Hant.json +++ b/homeassistant/components/growatt_server/translations/zh-Hant.json @@ -17,6 +17,7 @@ "data": { "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "title": "\u8f38\u5165 Growatt \u8cc7\u8a0a" diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index e836d81ac05..d6bb88f1dba 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Dominis a incloure" }, - "description": "Selecciona els dominis a incloure. S'inclouran totes les entitats del domini compatibles. Es crear\u00e0 una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "description": "Selecciona els dominis a incloure. S'inclouran totes les entitats del domini compatibles. Es crear\u00e0 una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV, control remot basat per activitat i c\u00e0mera.", "title": "Selecciona els dominis a incloure" } } diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 5814ad2069b..1213200474a 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Kaasatavad domeenid" }, - "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid. Iga telemeedia pleieri ja kaamera jaoks luuakse eraldi HomeKiti eksemplar tarvikure\u017eiimis.", + "description": "Vali kaasatavad domeenid. Lisatakse k\u00f5ik domeenis toetatud \u00fcksused. Iga tv-meediam\u00e4ngija, tegevusp\u00f5hise kaugjuhtimispuldi, luku ja kaamera jaoks luuakse eraldi HomeKit-instants lisaseadme re\u017eiimis.", "title": "Vali kaasatavad domeenid" } } diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 5ef479ff0dd..b6389567969 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -12,7 +12,7 @@ "data": { "include_domains": "\u5305\u542b\u7db2\u57df" }, - "description": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\uff0c\u6240\u6709\u8a72\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u5c07\u6703\u88ab\u5305\u542b\u3002 \u5176\u4ed6 Homekit \u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\u5be6\u4f8b\uff0c\u5c07\u6703\u4ee5\u914d\u4ef6\u6a21\u5f0f\u65b0\u589e\u3002", + "description": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\uff0c\u6240\u6709\u8a72\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u5c07\u6703\u88ab\u5305\u542b\u3002\u6bcf\u4e00\u500b\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u3001\u9060\u7aef\u9059\u63a7\u5668\u3001\u9580\u9396\u53ca\u651d\u5f71\u6a5f\uff0c\u5c07\u4ee5 Homekit \u914d\u4ef6\u6a21\u5f0f\u65b0\u589e\u3002", "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" } } diff --git a/homeassistant/components/honeywell/translations/fr.json b/homeassistant/components/honeywell/translations/fr.json new file mode 100644 index 00000000000..506e14ab26f --- /dev/null +++ b/homeassistant/components/honeywell/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/zh-Hant.json b/homeassistant/components/nfandroidtv/translations/zh-Hant.json new file mode 100644 index 00000000000..b16d55a44bd --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31" + }, + "description": "\u6b64\u6574\u5408\u9700\u8981\u5b89\u88dd Notifications for Android TV App\u3002\n\nAndroid TV \u7248\u672c\uff1ahttps://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV \u7248\u672c\uff1ahttps://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u8acb\u65bc\u8def\u7531\u5668\uff08\u8acb\u53c3\u8003\u8def\u7531\u5668\u624b\u518a\uff09\u4e2d\u8a2d\u5b9a\u4fdd\u7559\u88dd\u7f6e DHCP IP \u6216\u975c\u614b IP\u3002\u5047\u5982\u672a\u9032\u884c\u6b64\u8a2d\u5b9a\uff0c\u88dd\u7f6e\u53ef\u80fd\u6703\u8b8a\u6210\u4e0d\u53ef\u7528\u3002", + "title": "Android TV / Fire TV \u901a\u77e5" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index eab480073bc..b56ff193e33 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible." + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "wrong_server_id": "Server ID is not valid" }, "step": { "user": { diff --git a/homeassistant/components/switcher_kis/translations/fr.json b/homeassistant/components/switcher_kis/translations/fr.json new file mode 100644 index 00000000000..059be87fc07 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "step": { + "confirm": { + "description": "Voulez-vous commencer l'installation ?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 5d769dbe7d6..86c154e8567 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -29,6 +30,14 @@ "description": "M\u00f6chtest du {name} ({host}) einrichten?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Grund: {details}", + "title": "Synology DSM Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 361bd8de76e..ba4d0a88a6b 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -29,6 +29,13 @@ "description": "Voulez-vous configurer {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Raison: {details}" + }, "user": { "data": { "host": "Nom d'h\u00f4te ou adresse IP", diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 9b2a4726fc4..c4d466832e7 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -29,6 +30,14 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", "title": "\u7fa4\u6689 DSM" }, + "reauth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a73\u7d30\u8cc7\u8a0a\uff1a{details}", + "title": "Synology DSM \u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 49bb760ac2a..46737b8c79d 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -51,6 +51,13 @@ } } }, + "device_automation": { + "trigger_type": { + "event.notification.notification": "Envoyer une notification", + "event.value_notification.scene_activation": "Activation de la sc\u00e8ne sur {sous-type}", + "state.node_status": "Changement de statut du noeud" + } + }, "options": { "error": { "cannot_connect": "\u00c9chec de connexion", From 0b6e1d3d82ce62889cc77392333ce6429055a549 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 23 Jul 2021 04:01:24 +0200 Subject: [PATCH 526/818] Bump version (#53359) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 84ef65f3001..6c99f3c0786 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==5.2.1" + "pyatmo==5.2.3" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 021a20daea2..dff6eed22e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1319,7 +1319,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.2.1 +pyatmo==5.2.3 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d27ea0a42b0..ddd5245c82b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -750,7 +750,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.2.1 +pyatmo==5.2.3 # homeassistant.components.apple_tv pyatv==0.8.1 From 1d44bfcfb62c223b56256645952d7309a0b6c2d0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 23 Jul 2021 01:54:06 -0400 Subject: [PATCH 527/818] Use entity class attributes for Cert expiry (#53363) * Use entity class attributes for cert_expiry * Use entity class attributes for cert_expiry --- .../components/cert_expiry/sensor.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index a05acdb5d77..787465bb6f3 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -62,10 +62,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class CertExpiryEntity(CoordinatorEntity): """Defines a base Cert Expiry entity.""" - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return "mdi:certificate" + _attr_icon = "mdi:certificate" @property def extra_state_attributes(self): @@ -79,15 +76,13 @@ class CertExpiryEntity(CoordinatorEntity): class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): """Implementation of the Cert Expiry timestamp sensor.""" - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_TIMESTAMP + _attr_device_class = DEVICE_CLASS_TIMESTAMP - @property - def name(self): - """Return the name of the sensor.""" - return f"Cert Expiry Timestamp ({self.coordinator.name})" + def __init__(self, coordinator) -> None: + """Initialize a Cert Expiry timestamp sensor.""" + super().__init__(coordinator) + self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})" + self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" @property def state(self): @@ -95,8 +90,3 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): if self.coordinator.data: return self.coordinator.data.isoformat() return None - - @property - def unique_id(self): - """Return a unique id for the sensor.""" - return f"{self.coordinator.host}:{self.coordinator.port}-timestamp" From dee5d8903c537fa8f731633eec201c42cef52667 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 22 Jul 2021 23:17:39 -0700 Subject: [PATCH 528/818] Add motionEye switches (#52491) --- .../components/motioneye/__init__.py | 56 +++++- homeassistant/components/motioneye/camera.py | 74 +++---- homeassistant/components/motioneye/const.py | 1 + homeassistant/components/motioneye/switch.py | 120 ++++++++++++ tests/components/motioneye/__init__.py | 4 + tests/components/motioneye/test_camera.py | 3 +- tests/components/motioneye/test_switch.py | 180 ++++++++++++++++++ 7 files changed, 388 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/motioneye/switch.py create mode 100644 tests/components/motioneye/test_switch.py diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index d29f28e1704..282a24fae40 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import json import logging +from types import MappingProxyType from typing import Any, Callable from urllib.parse import urlencode, urljoin @@ -28,6 +29,7 @@ from motioneye_client.const import ( ) from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.webhook import ( async_generate_id, async_generate_path, @@ -49,8 +51,13 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.network import get_url -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( ATTR_EVENT_TYPE, @@ -78,7 +85,7 @@ from .const import ( ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [CAMERA_DOMAIN] +PLATFORMS = [CAMERA_DOMAIN, SWITCH_DOMAIN] def create_motioneye_client( @@ -420,3 +427,48 @@ async def handle_webhook( }, ) return None + + +class MotionEyeEntity(CoordinatorEntity): + """Base class for motionEye entities.""" + + def __init__( + self, + config_entry_id: str, + type_name: str, + camera: dict[str, Any], + client: MotionEyeClient, + coordinator: DataUpdateCoordinator, + options: MappingProxyType[str, Any], + enabled_by_default: bool = True, + ) -> None: + """Initialize a motionEye entity.""" + self._camera_id = camera[KEY_ID] + self._device_identifier = get_motioneye_device_identifier( + config_entry_id, self._camera_id + ) + self._unique_id = get_motioneye_entity_unique_id( + config_entry_id, + self._camera_id, + type_name, + ) + self._client = client + self._camera: dict[str, Any] | None = camera + self._options = options + self._enabled_by_default = enabled_by_default + super().__init__(coordinator) + + @property + def entity_registry_enabled_default(self) -> bool: + """Whether or not the entity is enabled by default.""" + return self._enabled_by_default + + @property + def unique_id(self) -> str: + """Return a unique id for this instance.""" + return self._unique_id + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return {"identifiers": {self._device_identifier}} diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index e3cad73dfc5..0727646b64d 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any, Dict, Optional +from types import MappingProxyType +from typing import Any import aiohttp from motioneye_client.client import MotionEyeClient from motioneye_client.const import ( DEFAULT_SURVEILLANCE_USERNAME, - KEY_ID, KEY_MOTION_DETECTION, KEY_NAME, KEY_STREAMING_AUTH_MODE, @@ -30,17 +30,12 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ( + MotionEyeEntity, get_camera_from_cameras, - get_motioneye_device_identifier, - get_motioneye_entity_unique_id, is_acceptable_camera, listen_for_new_cameras, ) @@ -79,6 +74,7 @@ async def async_setup_entry( camera, entry_data[CONF_CLIENT], entry_data[CONF_COORDINATOR], + entry.options, ) ] ) @@ -86,7 +82,7 @@ async def async_setup_entry( listen_for_new_cameras(hass, entry, camera_add) -class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any]]]): +class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): """motionEye mjpeg camera.""" def __init__( @@ -96,25 +92,26 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any password: str, camera: dict[str, Any], client: MotionEyeClient, - coordinator: DataUpdateCoordinator[dict[str, Any] | None], + coordinator: DataUpdateCoordinator, + options: MappingProxyType[str, str], ) -> None: """Initialize a MJPEG camera.""" self._surveillance_username = username self._surveillance_password = password - self._client = client - self._camera_id = camera[KEY_ID] - self._device_identifier = get_motioneye_device_identifier( - config_entry_id, self._camera_id - ) - self._unique_id = get_motioneye_entity_unique_id( - config_entry_id, self._camera_id, TYPE_MOTIONEYE_MJPEG_CAMERA - ) self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) - self._available = self._is_acceptable_streaming_camera(camera) # motionEye cameras are always streaming or unavailable. self.is_streaming = True + MotionEyeEntity.__init__( + self, + config_entry_id, + TYPE_MOTIONEYE_MJPEG_CAMERA, + camera, + client, + coordinator, + options, + ) MjpegCamera.__init__( self, { @@ -122,7 +119,6 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any **self._get_mjpeg_camera_properties_for_camera(camera), }, ) - CoordinatorEntity.__init__(self, coordinator) @callback def _get_mjpeg_camera_properties_for_camera( @@ -162,35 +158,26 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any if self._authentication == HTTP_BASIC_AUTHENTICATION: self._auth = aiohttp.BasicAuth(self._username, password=self._password) - @property - def unique_id(self) -> str: - """Return a unique id for this instance.""" - return self._unique_id - - @classmethod - def _is_acceptable_streaming_camera(cls, camera: dict[str, Any] | None) -> bool: + def _is_acceptable_streaming_camera(self) -> bool: """Determine if a camera is streaming/usable.""" - return is_acceptable_camera(camera) and MotionEyeClient.is_camera_streaming( - camera - ) + return is_acceptable_camera( + self._camera + ) and MotionEyeClient.is_camera_streaming(self._camera) @property def available(self) -> bool: """Return if entity is available.""" - return self._available + return super().available and self._is_acceptable_streaming_camera() @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - available = False - if self.coordinator.last_update_success: - camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) - if self._is_acceptable_streaming_camera(camera): - assert camera - self._set_mjpeg_camera_state_for_camera(camera) - self._motion_detection_enabled = camera.get(KEY_MOTION_DETECTION, False) - available = True - self._available = available + self._camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) + if self._camera and self._is_acceptable_streaming_camera(): + self._set_mjpeg_camera_state_for_camera(self._camera) + self._motion_detection_enabled = self._camera.get( + KEY_MOTION_DETECTION, False + ) super()._handle_coordinator_update() @property @@ -202,8 +189,3 @@ class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return self._motion_detection_enabled - - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return {"identifiers": {self._device_identifier}} diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index d918ca5ec23..41fb2c18d63 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -84,6 +84,7 @@ SIGNAL_CAMERA_ADD: Final = f"{DOMAIN}_camera_add_signal." "{}" SIGNAL_CAMERA_REMOVE: Final = f"{DOMAIN}_camera_remove_signal." "{}" TYPE_MOTIONEYE_MJPEG_CAMERA: Final = "motioneye_mjpeg_camera" +TYPE_MOTIONEYE_SWITCH_BASE: Final = f"{DOMAIN}_switch" WEB_HOOK_SENTINEL_KEY: Final = "src" WEB_HOOK_SENTINEL_VALUE: Final = "hass-motioneye" diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py new file mode 100644 index 00000000000..2f5d4e5c2b0 --- /dev/null +++ b/homeassistant/components/motioneye/switch.py @@ -0,0 +1,120 @@ +"""Switch platform for motionEye.""" +from __future__ import annotations + +from types import MappingProxyType +from typing import Any, Callable + +from motioneye_client.client import MotionEyeClient +from motioneye_client.const import ( + KEY_MOTION_DETECTION, + KEY_MOVIES, + KEY_NAME, + KEY_STILL_IMAGES, + KEY_TEXT_OVERLAY, + KEY_UPLOAD_ENABLED, + KEY_VIDEO_STREAMING, +) + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import MotionEyeEntity, listen_for_new_cameras +from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_SWITCH_BASE + +MOTIONEYE_SWITCHES = [ + (KEY_MOTION_DETECTION, "Motion Detection", True), + (KEY_TEXT_OVERLAY, "Text Overlay", False), + (KEY_VIDEO_STREAMING, "Video Streaming", False), + (KEY_STILL_IMAGES, "Still Images", True), + (KEY_MOVIES, "Movies", True), + (KEY_UPLOAD_ENABLED, "Upload Enabled", False), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> bool: + """Set up motionEye from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + + @callback + def camera_add(camera: dict[str, Any]) -> None: + """Add a new motionEye camera.""" + async_add_entities( + [ + MotionEyeSwitch( + entry.entry_id, + camera, + switch_key, + switch_key_friendly_name, + entry_data[CONF_CLIENT], + entry_data[CONF_COORDINATOR], + entry.options, + enabled, + ) + for switch_key, switch_key_friendly_name, enabled in MOTIONEYE_SWITCHES + ] + ) + + listen_for_new_cameras(hass, entry, camera_add) + return True + + +class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): + """MotionEyeSwitch switch class.""" + + def __init__( + self, + config_entry_id: str, + camera: dict[str, Any], + switch_key: str, + switch_key_friendly_name: str, + client: MotionEyeClient, + coordinator: DataUpdateCoordinator, + options: MappingProxyType[str, str], + enabled_by_default: bool, + ) -> None: + """Initialize the switch.""" + self._switch_key = switch_key + self._switch_key_friendly_name = switch_key_friendly_name + MotionEyeEntity.__init__( + self, + config_entry_id, + f"{TYPE_MOTIONEYE_SWITCH_BASE}_{switch_key}", + camera, + client, + coordinator, + options, + enabled_by_default, + ) + + @property + def name(self) -> str: + """Return the name of the switch.""" + camera_name = self._camera[KEY_NAME] if self._camera else "" + return f"{camera_name} {self._switch_key_friendly_name}" + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + return bool(self._camera and self._camera.get(self._switch_key, False)) + + async def _async_send_set_camera(self, value: bool) -> None: + """Set a switch value.""" + + # Fetch the very latest camera config to reduce the risk of updating with a + # stale configuration. + camera = await self._client.async_get_camera(self._camera_id) + if camera: + camera[self._switch_key] = value + await self._client.async_set_camera(self._camera_id, camera) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self._async_send_set_camera(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self._async_send_set_camera(False) diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index 8db3736aaef..dcc030e7e5b 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -133,6 +133,10 @@ TEST_CAMERA = { } TEST_CAMERAS = {"cameras": [TEST_CAMERA]} TEST_SURVEILLANCE_USERNAME = "surveillance_username" +TEST_SWITCH_ENTITY_ID_BASE = "switch.test_camera" +TEST_SWITCH_MOTION_DETECTION_ENTITY_ID = ( + f"{TEST_SWITCH_ENTITY_ID_BASE}_motion_detection" +) def create_mock_motioneye_client() -> AsyncMock: diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index af2fd3c365a..70c2d44436a 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -298,8 +298,7 @@ async def test_state_attributes(hass: HomeAssistant) -> None: async def test_device_info(hass: HomeAssistant) -> None: """Verify device information includes expected details.""" - client = create_mock_motioneye_client() - entry = await setup_mock_motioneye_config_entry(hass, client=client) + entry = await setup_mock_motioneye_config_entry(hass) device_identifier = get_motioneye_device_identifier(entry.entry_id, TEST_CAMERA_ID) device_registry = dr.async_get(hass) diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py new file mode 100644 index 00000000000..f406fe212b7 --- /dev/null +++ b/tests/components/motioneye/test_switch.py @@ -0,0 +1,180 @@ +"""Tests for the motionEye switch platform.""" +import copy +from datetime import timedelta +from unittest.mock import AsyncMock, call, patch + +from motioneye_client.const import ( + KEY_MOTION_DETECTION, + KEY_MOVIES, + KEY_STILL_IMAGES, + KEY_TEXT_OVERLAY, + KEY_UPLOAD_ENABLED, + KEY_VIDEO_STREAMING, +) + +from homeassistant.components.motioneye import get_motioneye_device_identifier +from homeassistant.components.motioneye.const import DEFAULT_SCAN_INTERVAL +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +import homeassistant.util.dt as dt_util + +from . import ( + TEST_CAMERA, + TEST_CAMERA_ID, + TEST_SWITCH_ENTITY_ID_BASE, + TEST_SWITCH_MOTION_DETECTION_ENTITY_ID, + create_mock_motioneye_client, + setup_mock_motioneye_config_entry, +) + +from tests.common import async_fire_time_changed + + +async def test_switch_turn_on_off(hass: HomeAssistant) -> None: + """Test turning the switch on and off.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + # Verify switch is on (as per TEST_COMPONENTS above). + entity_state = hass.states.get(TEST_SWITCH_MOTION_DETECTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + + client.async_get_camera = AsyncMock(return_value=TEST_CAMERA) + + expected_camera = copy.deepcopy(TEST_CAMERA) + expected_camera[KEY_MOTION_DETECTION] = False + + # When the next refresh is called return the updated values. + client.async_get_cameras = AsyncMock(return_value={"cameras": [expected_camera]}) + + # Turn switch off. + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_SWITCH_MOTION_DETECTION_ENTITY_ID}, + blocking=True, + ) + + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + # Verify correct parameters are passed to the library. + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera) + + # Verify the switch turns off. + entity_state = hass.states.get(TEST_SWITCH_MOTION_DETECTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "off" + + # When the next refresh is called return the updated values. + client.async_get_cameras = AsyncMock(return_value={"cameras": [TEST_CAMERA]}) + + # Turn switch on. + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_SWITCH_MOTION_DETECTION_ENTITY_ID}, + blocking=True, + ) + + # Verify correct parameters are passed to the library. + assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, TEST_CAMERA) + + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + # Verify the switch turns on. + entity_state = hass.states.get(TEST_SWITCH_MOTION_DETECTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + + +async def test_switch_has_correct_entities(hass: HomeAssistant) -> None: + """Test that the correct switch entities are created.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + enabled_switch_keys = [ + KEY_MOTION_DETECTION, + KEY_STILL_IMAGES, + KEY_MOVIES, + ] + disabled_switch_keys = [ + KEY_TEXT_OVERLAY, + KEY_UPLOAD_ENABLED, + KEY_VIDEO_STREAMING, + ] + + for switch_key in enabled_switch_keys: + entity_id = f"{TEST_SWITCH_ENTITY_ID_BASE}_{switch_key}" + entity_state = hass.states.get(entity_id) + assert entity_state, f"Couldn't find entity: {entity_id}" + + for switch_key in disabled_switch_keys: + entity_id = f"{TEST_SWITCH_ENTITY_ID_BASE}_{switch_key}" + entity_state = hass.states.get(entity_id) + assert not entity_state + + +async def test_disabled_switches_can_be_enabled(hass: HomeAssistant) -> None: + """Verify disabled switches can be enabled.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + disabled_switch_keys = [ + KEY_TEXT_OVERLAY, + KEY_UPLOAD_ENABLED, + ] + + for switch_key in disabled_switch_keys: + entity_id = f"{TEST_SWITCH_ENTITY_ID_BASE}_{switch_key}" + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == er.DISABLED_INTEGRATION + entity_state = hass.states.get(entity_id) + assert not entity_state + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=client, + ): + updated_entry = entity_registry.async_update_entity( + entity_id, disabled_by=None + ) + assert not updated_entry.disabled + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + entity_state = hass.states.get(entity_id) + assert entity_state + + +async def test_switch_device_info(hass: HomeAssistant) -> None: + """Verify device information includes expected details.""" + config_entry = await setup_mock_motioneye_config_entry(hass) + + device_identifer = get_motioneye_device_identifier( + config_entry.entry_id, TEST_CAMERA_ID + ) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device({device_identifer}) + assert device + + entity_registry = await er.async_get_registry(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + assert TEST_SWITCH_MOTION_DETECTION_ENTITY_ID in entities_from_device From 42e8a7c842e614b3e75ff80b02731f9fc60a683d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 23 Jul 2021 13:06:30 +0200 Subject: [PATCH 529/818] Move Fritzbox power, energy and temperature switch attributes to sensors (#52562) * deprecate switch entity properties * Add last_reset to FritzBoxEnergySensor * Remove obsolet temperature attribute --- homeassistant/components/fritzbox/const.py | 3 - homeassistant/components/fritzbox/sensor.py | 61 ++++++++++++++++++++- homeassistant/components/fritzbox/switch.py | 26 --------- tests/components/fritzbox/__init__.py | 4 ++ tests/components/fritzbox/test_switch.py | 28 ++++++---- 5 files changed, 82 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index af2ec30312f..6af75449a29 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -13,9 +13,6 @@ ATTR_STATE_WINDOW_OPEN: Final = "window_open" ATTR_TEMPERATURE_UNIT: Final = "temperature_unit" -ATTR_TOTAL_CONSUMPTION: Final = "total_consumption" -ATTR_TOTAL_CONSUMPTION_UNIT: Final = "total_consumption_unit" - CONF_CONNECTIONS: Final = "connections" CONF_COORDINATOR: Final = "coordinator" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 0a83e3ba60c..24b3d9cc5ff 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,6 +1,8 @@ """Support for AVM FRITZ!SmartHome temperature sensor only devices.""" from __future__ import annotations +from datetime import datetime + from homeassistant.components.sensor import ( ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, @@ -13,12 +15,17 @@ from homeassistant.const import ( ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.dt import utc_from_timestamp from . import FritzBoxEntity from .const import ( @@ -68,11 +75,39 @@ async def async_setup_entry( ) ) + if device.has_powermeter: + entities.append( + FritzBoxPowerSensor( + { + ATTR_NAME: f"{device.name} Power Consumption", + ATTR_ENTITY_ID: f"{device.ain}_power_consumption", + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + coordinator, + ain, + ) + ) + entities.append( + FritzBoxEnergySensor( + { + ATTR_NAME: f"{device.name} Total Energy", + ATTR_ENTITY_ID: f"{device.ain}_total_energy", + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: None, + }, + coordinator, + ain, + ) + ) + async_add_entities(entities) class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): - """The entity class for FRITZ!SmartHome sensors.""" + """The entity class for FRITZ!SmartHome battery sensors.""" @property def state(self) -> int | None: @@ -80,6 +115,30 @@ class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): return self.device.battery_level # type: ignore [no-any-return] +class FritzBoxPowerSensor(FritzBoxEntity, SensorEntity): + """The entity class for FRITZ!SmartHome power consumption sensors.""" + + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + return self.device.power / 1000 # type: ignore [no-any-return] + + +class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): + """The entity class for FRITZ!SmartHome total energy sensors.""" + + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + return (self.device.energy or 0.0) / 1000 + + @property + def last_reset(self) -> datetime: + """Return the time when the sensor was last reset, if any.""" + # device does not provide timestamp of initialization + return utc_from_timestamp(0) + + class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): """The entity class for FRITZ!SmartHome temperature sensors.""" diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 22b2adf5800..62d1cb1ecf8 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -10,10 +10,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_NAME, - ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -22,16 +19,11 @@ from . import FritzBoxEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, - ATTR_TEMPERATURE_UNIT, - ATTR_TOTAL_CONSUMPTION, - ATTR_TOTAL_CONSUMPTION_UNIT, CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) from .model import SwitchExtraAttributes -ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -91,22 +83,4 @@ class FritzboxSwitch(FritzBoxEntity, SwitchEntity): ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, ATTR_STATE_LOCKED: self.device.lock, } - - if self.device.has_powermeter: - attrs[ - ATTR_TOTAL_CONSUMPTION - ] = f"{((self.device.energy or 0.0) / 1000):.3f}" - attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = ATTR_TOTAL_CONSUMPTION_UNIT_VALUE - if self.device.has_temperature_sensor: - attrs[ATTR_TEMPERATURE] = str( - self.hass.config.units.temperature( - self.device.temperature, TEMP_CELSIUS - ) - ) - attrs[ATTR_TEMPERATURE_UNIT] = self.hass.config.units.temperature_unit return attrs - - @property - def current_power_w(self) -> float: - """Return the current power usage in W.""" - return self.device.power / 1000 # type: ignore [no-any-return] diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 3ff4b71364e..da6bd982d9d 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -55,6 +55,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): battery_level = 23 fw_version = "1.2.3" has_alarm = True + has_powermeter = False has_switch = False has_temperature_sensor = False has_thermostat = False @@ -73,6 +74,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): eco_temperature = 16.0 fw_version = "1.2.3" has_alarm = False + has_powermeter = False has_switch = False has_temperature_sensor = False has_thermostat = True @@ -91,6 +93,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock): device_lock = "fake_locked_device" fw_version = "1.2.3" has_alarm = False + has_powermeter = False has_switch = False has_temperature_sensor = True has_thermostat = False @@ -107,6 +110,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): energy = 1234 fw_version = "1.2.3" has_alarm = False + has_powermeter = True has_switch = True has_temperature_sensor = True has_thermostat = False diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 4bace3834fb..cb6ae85f889 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -7,24 +7,22 @@ from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, - ATTR_TEMPERATURE_UNIT, - ATTR_TOTAL_CONSUMPTION, - ATTR_TOTAL_CONSUMPTION_UNIT, DOMAIN as FB_DOMAIN, ) from homeassistant.components.sensor import ( + ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, ) -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, DOMAIN +from homeassistant.components.switch import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, ENERGY_KILO_WATT_HOUR, + POWER_WATT, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, @@ -51,14 +49,9 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678 assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" - assert state.attributes[ATTR_TEMPERATURE] == "1.23" - assert state.attributes[ATTR_TEMPERATURE_UNIT] == TEMP_CELSIUS - assert state.attributes[ATTR_TOTAL_CONSUMPTION] == "1.234" - assert state.attributes[ATTR_TOTAL_CONSUMPTION_UNIT] == ENERGY_KILO_WATT_HOUR assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature") @@ -70,6 +63,21 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption") + assert state + assert state.state == "5.678" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Power Consumption" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") + assert state + assert state.state == "1.234" + assert state.attributes[ATTR_LAST_RESET] == "1970-01-01T00:00:00+00:00" + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert ATTR_STATE_CLASS not in state.attributes + async def test_turn_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" From 0d38ee7378b6c8f3e3e34326e1eec3bbd8460db6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Jul 2021 14:03:46 +0200 Subject: [PATCH 530/818] Upgrade wled to 0.8.0 (#53376) --- homeassistant/components/wled/light.py | 5 ++++- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 4326f1066c7..f4251a90343 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -5,6 +5,7 @@ from functools import partial from typing import Any, Tuple, cast import voluptuous as vol +from wled import Playlist from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -212,7 +213,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" - playlist: int | None = self.coordinator.data.state.playlist + playlist: int | Playlist | None = self.coordinator.data.state.playlist + if isinstance(playlist, Playlist): + playlist = playlist.playlist_id if playlist == -1: playlist = None diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index dbe13fe56ca..5ece2d4b9d8 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.7.3"], + "requirements": ["wled==0.8.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index dff6eed22e9..1c6d1356f5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,7 +2381,7 @@ wirelesstagpy==0.5.0 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.3 +wled==0.8.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddd5245c82b..e0919445fdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1305,7 +1305,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.3 +wled==0.8.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 9ee7e55f10961ac8703e1d4c17106cbbe79b1f19 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 23 Jul 2021 10:34:49 -0400 Subject: [PATCH 531/818] Send initial status in zwave_js WS API cmds to subscribe to updates (#53386) --- homeassistant/components/zwave_js/api.py | 12 +++- tests/components/zwave_js/test_api.py | 73 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1e1dc2382fc..379376bb98d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -796,7 +796,7 @@ async def websocket_subscribe_heal_network_progress( controller.on("heal network done", partial(forward_event, "result")), ] - connection.send_result(msg[ID]) + connection.send_result(msg[ID], controller.heal_network_progress) @websocket_api.require_admin @@ -1390,7 +1390,15 @@ async def websocket_subscribe_firmware_update_status( ] connection.subscriptions[msg["id"]] = async_cleanup - connection.send_result(msg[ID]) + result = ( + { + "sent_fragments": node.firmware_update_progress.sent_fragments, + "total_fragments": node.firmware_update_progress.total_fragments, + } + if node.firmware_update_progress + else None + ) + connection.send_result(msg[ID], result) class FirmwareUploadView(HomeAssistantView): diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 80fe6da90f5..a96e76be865 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -929,6 +929,7 @@ async def test_subscribe_heal_network_progress( msg = await ws_client.receive_json() assert msg["success"] + assert msg["result"] is None # Fire heal network progress event = Event( @@ -961,6 +962,39 @@ async def test_subscribe_heal_network_progress( assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_subscribe_heal_network_progress_initial_value( + hass, integration, client, hass_ws_client +): + """Test subscribe_heal_network_progress command when heal network in progress.""" + entry = integration + ws_client = await hass_ws_client(hass) + + assert not client.driver.controller.heal_network_progress + + # Fire heal network progress before sending heal network progress command + event = Event( + "heal network progress", + { + "source": "controller", + "event": "heal network progress", + "progress": {67: "pending"}, + }, + ) + client.driver.controller.receive_event(event) + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_heal_network_progress", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == {"67": "pending"} + + async def test_stop_healing_network( hass, integration, @@ -2403,6 +2437,7 @@ async def test_subscribe_firmware_update_status( msg = await ws_client.receive_json() assert msg["success"] + assert msg["result"] is None event = Event( type="firmware update progress", @@ -2443,6 +2478,44 @@ async def test_subscribe_firmware_update_status( } +async def test_subscribe_firmware_update_status_initial_value( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test subscribe_firmware_update_status websocket command with in progress update.""" + entry = integration + ws_client = await hass_ws_client(hass) + + assert multisensor_6.firmware_update_progress is None + + # Send a firmware update progress event before the WS command + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": multisensor_6.node_id, + "sentFragments": 1, + "totalFragments": 10, + }, + ) + multisensor_6.receive_event(event) + + client.async_send_command_no_wait.return_value = {} + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == {"sent_fragments": 1, "total_fragments": 10} + + async def test_subscribe_firmware_update_status_failures( hass, integration, multisensor_6, client, hass_ws_client ): From 952cb964c8504badc55a0b5a6b40a36f37ba0fd0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 23 Jul 2021 10:35:44 -0400 Subject: [PATCH 532/818] Add new input to zwave_js.multicast_set_value service (#53369) * Tweak Z-Wave JS service and WS API commands * Revert WS API change so it can be split out * Add keywords --- homeassistant/components/zwave_js/services.py | 11 +- .../components/zwave_js/services.yaml | 8 +- tests/components/zwave_js/test_light.py | 1 + tests/components/zwave_js/test_services.py | 100 ++++++++++++++---- .../light_color_null_values_state.json | 14 ++- tests/fixtures/zwave_js/zen_31_state.json | 15 ++- 6 files changed, 112 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index e09b0fb0c5e..fa0e93a72aa 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -283,6 +283,7 @@ class ZWaveServices: ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, + vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA}, }, vol.Any( cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), @@ -409,6 +410,7 @@ class ZWaveServices: """Set a value via multicast to multiple nodes.""" nodes = service.data[const.ATTR_NODES] broadcast: bool = service.data[const.ATTR_BROADCAST] + options = service.data.get(const.ATTR_OPTIONS) if not broadcast and len(nodes) == 1: const.LOGGER.warning( @@ -438,10 +440,11 @@ class ZWaveServices: client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT] success = await async_multicast_set_value( - client, - new_value, - {k: v for k, v in value.items() if v is not None}, - None if broadcast else list(nodes), + client=client, + new_value=new_value, + value_data={k: v for k, v in value.items() if v is not None}, + nodes=None if broadcast else list(nodes), + options=options, ) if success is False: diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 7d88f5f7830..f737f159806 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -156,7 +156,7 @@ set_value: object: options: name: Options - description: Set value options. Refer to the Z-Wave JS documentation for more information on what options can be set. + description: Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set. required: false selector: object: @@ -209,6 +209,12 @@ multicast_set_value: required: false selector: text: + options: + name: Options + description: Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set. + required: false + selector: + object: value: name: Value description: The new value to set. diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 43250c3477d..fa3c73a9a42 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -569,6 +569,7 @@ async def test_rgbw_light(hass, client, zen_31, integration): "type": "any", "readable": True, "writeable": True, + "valueChangeOptions": ["transitionDuration"], }, "value": {"blue": 70, "green": 159, "red": 255, "warmWhite": 141}, } diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 4b66e178e6f..3ee656e40c0 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -13,6 +13,7 @@ from homeassistant.components.zwave_js.const import ( ATTR_CONFIG_VALUE, ATTR_OPTIONS, ATTR_PROPERTY, + ATTR_PROPERTY_KEY, ATTR_REFRESH_ALL_VALUES, ATTR_VALUE, ATTR_WAIT_FOR_RESULT, @@ -34,6 +35,7 @@ from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .common import ( AEON_SMART_SWITCH_LIGHT_ENTITY, AIR_TEMPERATURE_SENSOR, + BULB_6_MULTI_COLOR_LIGHT_ENTITY, CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY, @@ -819,8 +821,9 @@ async def test_multicast_set_value( CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, ], - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, @@ -834,8 +837,9 @@ async def test_multicast_set_value( climate_danfoss_lc_13.node_id, ] assert args["valueId"] == { - "commandClass": 117, - "property": "local", + "commandClass": 67, + "property": "setpoint", + "propertyKey": 1, } assert args["value"] == 2 @@ -850,8 +854,9 @@ async def test_multicast_set_value( CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, ], - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: "0x2", }, blocking=True, @@ -865,8 +870,9 @@ async def test_multicast_set_value( climate_danfoss_lc_13.node_id, ] assert args["valueId"] == { - "commandClass": 117, - "property": "local", + "commandClass": 67, + "property": "setpoint", + "propertyKey": 1, } assert args["value"] == 2 @@ -878,8 +884,9 @@ async def test_multicast_set_value( SERVICE_MULTICAST_SET_VALUE, { ATTR_BROADCAST: True, - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, @@ -889,8 +896,9 @@ async def test_multicast_set_value( args = client.async_send_command.call_args[0][0] assert args["command"] == "broadcast_node.set_value" assert args["valueId"] == { - "commandClass": 117, - "property": "local", + "commandClass": 67, + "property": "setpoint", + "propertyKey": 1, } assert args["value"] == 2 @@ -902,8 +910,9 @@ async def test_multicast_set_value( SERVICE_MULTICAST_SET_VALUE, { ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, @@ -921,8 +930,9 @@ async def test_multicast_set_value( DOMAIN, SERVICE_MULTICAST_SET_VALUE, { - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, @@ -940,8 +950,9 @@ async def test_multicast_set_value( CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, ], - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, @@ -965,8 +976,9 @@ async def test_multicast_set_value( CLIMATE_DANFOSS_LC13_ENTITY, ], ATTR_DEVICE_ID: "fake_device_id", - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, @@ -982,14 +994,58 @@ async def test_multicast_set_value( SERVICE_MULTICAST_SET_VALUE, { ATTR_BROADCAST: True, - ATTR_COMMAND_CLASS: 117, - ATTR_PROPERTY: "local", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, ATTR_VALUE: 2, }, blocking=True, ) +async def test_multicast_set_value_options( + hass, + client, + bulb_6_multi_color, + light_color_null_values, + integration, +): + """Test multicast_set_value service with options.""" + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: [ + BULB_6_MULTI_COLOR_LIGHT_ENTITY, + "light.repeater", + ], + ATTR_COMMAND_CLASS: 51, + ATTR_PROPERTY: "targetColor", + ATTR_PROPERTY_KEY: 2, + ATTR_VALUE: 2, + ATTR_OPTIONS: {"transitionDuration": 1}, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "multicast_group.set_value" + assert args["nodeIDs"] == [ + bulb_6_multi_color.node_id, + light_color_null_values.node_id, + ] + assert args["valueId"] == { + "commandClass": 51, + "property": "targetColor", + "propertyKey": 2, + } + assert args["value"] == 2 + assert args["options"] == {"transitionDuration": 1} + + client.async_send_command.reset_mock() + + async def test_ping( hass, client, diff --git a/tests/fixtures/zwave_js/light_color_null_values_state.json b/tests/fixtures/zwave_js/light_color_null_values_state.json index f60b61c7a5a..213b873f85c 100644 --- a/tests/fixtures/zwave_js/light_color_null_values_state.json +++ b/tests/fixtures/zwave_js/light_color_null_values_state.json @@ -1,5 +1,5 @@ { - "nodeId": 39, + "nodeId": 40, "index": 0, "installerIcon": 6912, "userIcon": 6912, @@ -297,7 +297,8 @@ "description": "The target value of the Red color.", "label": "Target value (Red)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { @@ -316,7 +317,8 @@ "description": "The target value of the Green color.", "label": "Target value (Green)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { @@ -335,7 +337,8 @@ "description": "The target value of the Blue color.", "label": "Target value (Blue)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { @@ -349,7 +352,8 @@ "type": "any", "readable": true, "writeable": true, - "label": "Target Color" + "label": "Target Color", + "valueChangeOptions": ["transitionDuration"] } }, { diff --git a/tests/fixtures/zwave_js/zen_31_state.json b/tests/fixtures/zwave_js/zen_31_state.json index f0c3c1759eb..7407607e086 100644 --- a/tests/fixtures/zwave_js/zen_31_state.json +++ b/tests/fixtures/zwave_js/zen_31_state.json @@ -1645,7 +1645,8 @@ "type": "any", "readable": true, "writeable": true, - "label": "Target Color" + "label": "Target Color", + "valueChangeOptions": ["transitionDuration"] }, "value": { "warmWhite": 141, @@ -1670,7 +1671,8 @@ "description": "The target value of the Warm White color.", "label": "Target value (Warm White)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { @@ -1689,7 +1691,8 @@ "description": "The target value of the Red color.", "label": "Target value (Red)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { @@ -1708,7 +1711,8 @@ "description": "The target value of the Green color.", "label": "Target value (Green)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { @@ -1727,7 +1731,8 @@ "description": "The target value of the Blue color.", "label": "Target value (Blue)", "min": 0, - "max": 255 + "max": 255, + "valueChangeOptions": ["transitionDuration"] } }, { From 87165d61331182f98040199893ab1ca51adad63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=B8rge=20Nordli?= Date: Fri, 23 Jul 2021 18:45:31 +0200 Subject: [PATCH 533/818] Support group events for AC switches and binary sensors. Fixes #53065. (#53384) * Support group events for AC switches and binary sensors. Fixes #53065. * Review comments --- homeassistant/components/rfxtrx/__init__.py | 13 +++++ .../components/rfxtrx/binary_sensor.py | 2 +- homeassistant/components/rfxtrx/const.py | 7 +++ homeassistant/components/rfxtrx/switch.py | 8 +-- tests/components/rfxtrx/test_binary_sensor.py | 16 +++++- tests/components/rfxtrx/test_switch.py | 56 +++++++++++++++++++ 6 files changed, 93 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 66d4235ffdb..44e1d537408 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -44,6 +44,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_EVENT, + COMMAND_GROUP_LIST, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEBUG, @@ -465,6 +466,9 @@ class RfxtrxEntity(RestoreEntity): self._event = event self._device_id = device_id self._unique_id = "_".join(x for x in self._device_id) + # If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to + # group events regardless of their group indices. + (self._group_id, _, _) = device.id_string.partition(":") async def async_added_to_hass(self): """Restore RFXtrx device state (ON/OFF).""" @@ -520,6 +524,15 @@ class RfxtrxEntity(RestoreEntity): "model": self._device.type_string, } + def _event_applies(self, event, device_id): + """Check if event applies to me.""" + if "Command" in event.values and event.values["Command"] in COMMAND_GROUP_LIST: + (group_id, _, _) = event.device.id_string.partition(":") + return group_id == self._group_id + + # Otherwise, the event only applies to the matching device. + return device_id == self._device_id + def _apply_event(self, event): """Apply a received event.""" self._event = event diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 78eb49740d5..9e3d24cdb6a 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -233,7 +233,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" - if device_id != self._device_id: + if not self._event_applies(event, device_id): return _LOGGER.debug( diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 1f36b00e184..d457435f85c 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -19,6 +19,7 @@ COMMAND_ON_LIST = [ "On", "Up", "Stop", + "Group on", "Open (inline relay)", "Stop (inline relay)", "Enable sun automation", @@ -26,11 +27,17 @@ COMMAND_ON_LIST = [ COMMAND_OFF_LIST = [ "Off", + "Group off", "Down", "Close (inline relay)", "Disable sun automation", ] +COMMAND_GROUP_LIST = [ + "Group on", + "Group off", +] + ATTR_EVENT = "event" SERVICE_SEND = "send" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 96c066d5f3e..60ddb9a4d16 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -117,12 +117,10 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" - if device_id != self._device_id: - return + if self._event_applies(event, device_id): + self._apply_event(event) - self._apply_event(event) - - self.async_write_ha_state() + self.async_write_ha_state() @property def is_on(self): diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index a52b390395a..5b76c6287a3 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -121,7 +121,7 @@ async def test_several(hass, rfxtrx): devices={ "0b1100cd0213c7f230010f71": {}, "0b1100100118cdea02010f70": {}, - "0b1100101118cdea02010f70": {}, + "0b1100100118cdea03010f70": {}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -141,10 +141,20 @@ async def test_several(hass, rfxtrx): assert state.state == "off" assert state.attributes.get("friendly_name") == "AC 118cdea:2" - state = hass.states.get("binary_sensor.ac_1118cdea_2") + state = hass.states.get("binary_sensor.ac_118cdea_3") assert state assert state.state == "off" - assert state.attributes.get("friendly_name") == "AC 1118cdea:2" + assert state.attributes.get("friendly_name") == "AC 118cdea:3" + + # "2: Group on" + await rfxtrx.signal("0b1100100118cdea03040f70") + assert hass.states.get("binary_sensor.ac_118cdea_2").state == "on" + assert hass.states.get("binary_sensor.ac_118cdea_3").state == "on" + + # "2: Group off" + await rfxtrx.signal("0b1100100118cdea03030f70") + assert hass.states.get("binary_sensor.ac_118cdea_2").state == "off" + assert hass.states.get("binary_sensor.ac_118cdea_3").state == "off" async def test_discover(hass, rfxtrx_automatic): diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 12064911bb6..94adf4a980e 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -125,6 +125,62 @@ async def test_repetitions(hass, rfxtrx, repetitions): assert rfxtrx.transport.send.call_count == repetitions +async def test_switch_events(hass, rfxtrx): + """Event test with 2 switches.""" + entry_data = create_rfx_test_cfg( + devices={ + "0b1100cd0213c7f205010f51": {"signal_repetitions": 1}, + "0b1100cd0213c7f210010f51": {"signal_repetitions": 1}, + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.ac_213c7f2_16") + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "AC 213c7f2:16" + + state = hass.states.get("switch.ac_213c7f2_5") + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "AC 213c7f2:5" + + # "16: On" + await rfxtrx.signal("0b1100100213c7f210010f70") + assert hass.states.get("switch.ac_213c7f2_5").state == "off" + assert hass.states.get("switch.ac_213c7f2_16").state == "on" + + # "16: Off" + await rfxtrx.signal("0b1100100213c7f210000f70") + assert hass.states.get("switch.ac_213c7f2_5").state == "off" + assert hass.states.get("switch.ac_213c7f2_16").state == "off" + + # "5: On" + await rfxtrx.signal("0b1100100213c7f205010f70") + assert hass.states.get("switch.ac_213c7f2_5").state == "on" + assert hass.states.get("switch.ac_213c7f2_16").state == "off" + + # "5: Off" + await rfxtrx.signal("0b1100100213c7f205000f70") + assert hass.states.get("switch.ac_213c7f2_5").state == "off" + assert hass.states.get("switch.ac_213c7f2_16").state == "off" + + # "16: Group on" + await rfxtrx.signal("0b1100100213c7f210040f70") + assert hass.states.get("switch.ac_213c7f2_5").state == "on" + assert hass.states.get("switch.ac_213c7f2_16").state == "on" + + # "16: Group off" + await rfxtrx.signal("0b1100100213c7f210030f70") + assert hass.states.get("switch.ac_213c7f2_5").state == "off" + assert hass.states.get("switch.ac_213c7f2_16").state == "off" + + async def test_discover_switch(hass, rfxtrx_automatic): """Test with discovery of switches.""" rfxtrx = rfxtrx_automatic From d8887a97e3befcd7d06c562f2f30a6cf8047c61b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Jul 2021 18:57:36 +0200 Subject: [PATCH 534/818] Upgrade debugpy to 1.4.0 (#53284) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index b82f544329c..a67d7181a90 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.3.0"], + "requirements": ["debugpy==1.4.0"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1c6d1356f5b..35fb72f2e21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.3.0 +debugpy==1.4.0 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0919445fdc..36c198aca5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.3.0 +debugpy==1.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 91018d0451dc6d21f0f20d326db0ee2a37c27507 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 23 Jul 2021 18:37:18 +0100 Subject: [PATCH 535/818] Add support for power data from Koogeek SW2 via homekit_controller (#53378) --- .../components/homekit_controller/const.py | 1 + .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/sensor.py | 6 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_koogeek_sw2.py | 64 +++++ .../homekit_controller/koogeek_sw2.json | 265 ++++++++++++++++++ 7 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py create mode 100644 tests/fixtures/homekit_controller/koogeek_sw2.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 9fbc8fc4c62..5ac777a3969 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -46,5 +46,6 @@ HOMEKIT_ACCESSORY_DISPATCH = { CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: "sensor", CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): "sensor", } diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 4bc61f53cc0..a4644d0e34a 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.5.1"], + "requirements": ["aiohomekit==0.6.0"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index b21010e9b1e..c24f46198c0 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -37,6 +37,12 @@ SIMPLE_SENSOR = { "state_class": STATE_CLASS_MEASUREMENT, "unit": "watts", }, + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: { + "name": "Real Time Energy", + "device_class": DEVICE_CLASS_POWER, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": "watts", + }, CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): { "name": "Current Temperature", "device_class": DEVICE_CLASS_TEMPERATURE, diff --git a/requirements_all.txt b/requirements_all.txt index 35fb72f2e21..4b07a6e6d70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aioguardian==1.0.8 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.5.1 +aiohomekit==0.6.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36c198aca5c..51ca22f2b03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioguardian==1.0.8 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.5.1 +aiohomekit==0.6.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py new file mode 100644 index 00000000000..00057822071 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -0,0 +1,64 @@ +""" +Make sure that existing Koogeek SW2 is enumerated correctly. + +This Koogeek device has a custom power sensor that extra handling. + +It should have 2 entities - the actual switch and a sensor for power usage. +""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_koogeek_ls1_setup(hass): + """Test that a Koogeek LS1 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "koogeek_sw2.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + + # Assert that the switch entity is correctly added to the entity registry + entry = entity_registry.async_get("switch.koogeek_sw2_187a91") + assert entry.unique_id == "homekit-CNNT061751001372-8" + + helper = Helper( + hass, "switch.koogeek_sw2_187a91", pairing, accessories[0], config_entry + ) + state = await helper.poll_and_get_state() + + # Assert that the friendly name is detected correctly + assert state.attributes["friendly_name"] == "Koogeek-SW2-187A91" + + device_registry = dr.async_get(hass) + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Koogeek" + assert device.name == "Koogeek-SW2-187A91" + assert device.model == "KH02CN" + assert device.sw_version == "1.0.3" + assert device.via_device_id is None + + # Assert that the power sensor entity is correctly added to the entity registry + entry = entity_registry.async_get("sensor.koogeek_sw2_187a91_real_time_energy") + assert entry.unique_id == "homekit-CNNT061751001372-aid:1-sid:14-cid:14" + + helper = Helper( + hass, + "sensor.koogeek_sw2_187a91_real_time_energy", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + + # Assert that the friendly name is detected correctly + assert state.attributes["friendly_name"] == "Koogeek-SW2-187A91 - Real Time Energy" + + device_registry = dr.async_get(hass) + + assert device.id == entry.device_id diff --git a/tests/fixtures/homekit_controller/koogeek_sw2.json b/tests/fixtures/homekit_controller/koogeek_sw2.json new file mode 100644 index 00000000000..b7807bfb6a7 --- /dev/null +++ b/tests/fixtures/homekit_controller/koogeek_sw2.json @@ -0,0 +1,265 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Koogeek-SW2-187A91" + }, + { + "format": "string", + "iid": 3, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Koogeek" + }, + { + "format": "string", + "iid": 4, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "KH02CN" + }, + { + "format": "string", + "iid": 5, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "CNNT061751001372" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 7, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.0.3" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": true, + "format": "bool", + "iid": 9, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 10, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Switch 1" + } + ], + "iid": 8, + "primary": true, + "stype": "switch", + "type": "00000049-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": true, + "format": "bool", + "iid": 12, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 13, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Switch 2" + } + ], + "iid": 11, + "primary": true, + "stype": "switch", + "type": "00000049-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 15, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "custom service" + }, + { + "description": "Current Time", + "format": "int", + "iid": 16, + "perms": [ + "pr" + ], + "type": "7BBBA961-EB2D-11E5-A837-0800200C9A66", + "value": 1599731035 + }, + { + "description": "Time Zone", + "format": "int", + "iid": 17, + "perms": [ + "pr", + "pw" + ], + "type": "7BBBA980-EB2D-11E5-A837-0800200C9A66", + "value": 16 + }, + { + "description": "Current Power", + "ev": false, + "format": "int", + "iid": 18, + "perms": [ + "pr", + "ev" + ], + "type": "7BBBA96E-EB2D-11E5-A837-0800200C9A66", + "value": 0 + }, + { + "description": "Power Consumption Today", + "format": "data", + "iid": 19, + "perms": [ + "pr" + ], + "type": "7BBBA96F-EB2D-11E5-A837-0800200C9A66", + "value": "9pcBAL4GAAC1BgAAtgYAAELhAABXIwAAtgYAAKcGAABHOQAA1aMAAP//////////////////////////////////////////////////////////////////////////" + }, + { + "description": "Power Consumption last 2 Month", + "format": "data", + "iid": 20, + "perms": [ + "pr" + ], + "type": "7BBBA972-EB2D-11E5-A837-0800200C9A66", + "value": "/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCFAEA5HkIADGbAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "Power Consumption last 12 Month", + "format": "data", + "iid": 21, + "perms": [ + "pr" + ], + "type": "7BBBA970-EB2D-11E5-A837-0800200C9A66", + "value": "//////////////////////////////////////////+XKQ0A////////////////" + } + ], + "hidden": true, + "iid": 14, + "stype": "Unknown Service: 7BBBA977-EB2D-11E5-A837-0800200C9A66", + "type": "7BBBA977-EB2D-11E5-A837-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 23, + "perms": [ + "pr", + "hd" + ], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 24, + "maxLen": 256, + "perms": [ + "pw", + "hd" + ], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 25, + "perms": [ + "pr", + "ev", + "hd" + ], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 26, + "perms": [ + "pw", + "hd" + ], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "hidden": true, + "iid": 22, + "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", + "type": "151909D0-3802-11E4-916C-0800200C9A66" + } + ] + } +] \ No newline at end of file From 4b353917f56bc97543cfabf90e9c19df220e5c23 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 23 Jul 2021 13:00:02 -0600 Subject: [PATCH 536/818] Enforce strict typing for Notion (#53355) * Enforce strict typing for Notion * Code review --- .strict-typing | 1 + homeassistant/components/notion/__init__.py | 18 ++++++++++++------ mypy.ini | 14 +++++++++++--- script/hassfest/mypy_config.py | 1 - 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index 19835bfd777..0a226f973f6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -65,6 +65,7 @@ homeassistant.components.netatmo.* homeassistant.components.network.* homeassistant.components.no_ip.* homeassistant.components.notify.* +homeassistant.components.notion.* homeassistant.components.number.* homeassistant.components.onewire.* homeassistant.components.persistent_notification.* diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 8acf9c24d4a..aab10916514 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -1,6 +1,9 @@ """Support for Notion.""" +from __future__ import annotations + import asyncio from datetime import timedelta +from typing import Any from aionotion import async_get_client from aionotion.errors import InvalidCredentialsError, NotionError @@ -55,9 +58,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - async def async_update(): + async def async_update() -> dict[str, dict[str, Any]]: """Get the latest data from the Notion API.""" - data = {"bridges": {}, "sensors": {}, "tasks": {}} + data: dict[str, dict[str, Any]] = {"bridges": {}, "sensors": {}, "tasks": {}} tasks = { "bridges": client.bridge.async_all(), "sensors": client.sensor.async_all(), @@ -111,7 +114,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_register_new_bridge( hass: HomeAssistant, bridge: dict, entry: ConfigEntry -): +) -> None: """Register a new bridge.""" device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( @@ -190,13 +193,16 @@ class NotionEntity(CoordinatorEntity): self._bridge_id = sensor["bridge"]["id"] device_registry = await dr.async_get_registry(self.hass) + this_device = device_registry.async_get_device( + {(DOMAIN, sensor["hardware_id"])} + ) bridge = self.coordinator.data["bridges"][self._bridge_id] bridge_device = device_registry.async_get_device( {(DOMAIN, bridge["hardware_id"])} ) - this_device = device_registry.async_get_device( - {(DOMAIN, sensor["hardware_id"])} - ) + + if not bridge_device or not this_device: + return device_registry.async_update_device( this_device.id, via_device_id=bridge_device.id diff --git a/mypy.ini b/mypy.ini index e0bdc372f50..014272f0022 100644 --- a/mypy.ini +++ b/mypy.ini @@ -726,6 +726,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.notion.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.number.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1422,9 +1433,6 @@ ignore_errors = true [mypy-homeassistant.components.norway_air.*] ignore_errors = true -[mypy-homeassistant.components.notion.*] -ignore_errors = true - [mypy-homeassistant.components.nsw_fuel_station.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b1c74fceb45..642dd47b732 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -114,7 +114,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.nilu.*", "homeassistant.components.nmap_tracker.*", "homeassistant.components.norway_air.*", - "homeassistant.components.notion.*", "homeassistant.components.nsw_fuel_station.*", "homeassistant.components.nuki.*", "homeassistant.components.nws.*", From 0d5e480397ac96fa2cda7d25ff255241bdca01b0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 23 Jul 2021 21:18:19 +0200 Subject: [PATCH 537/818] Fix Gira and Jung models not working as deCONZ device triggers (#53394) --- homeassistant/components/deconz/device_trigger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index d7e42808851..40a49e111d8 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -566,9 +566,9 @@ REMOTES = { AQARA_OPPLE_6_BUTTONS_MODEL: AQARA_OPPLE_6_BUTTONS, DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL: DRESDEN_ELEKTRONIK_LIGHTING_SWITCH, DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL: DRESDEN_ELEKTRONIK_SCENE_SWITCH, - GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, - GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, - JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH, + GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH, + JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH, LIDL_SILVERCREST_DOORBELL_MODEL: LIDL_SILVERCREST_DOORBELL, LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, From d0bef974539d4be8490211ef9c544f67ddbecece Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 23 Jul 2021 21:43:00 +0200 Subject: [PATCH 538/818] Handle situations where action is part of a deCONZ event but has the value None (#53373) * Handle situations where action is part of an event but has the value None * Cover more possible permutations of what a bad action string is --- .../components/deconz/deconz_event.py | 6 ++- tests/components/deconz/test_deconz_event.py | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 872dc3688c2..c5336825878 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -163,11 +163,13 @@ class DeconzAlarmEvent(DeconzEvent): if ( self.gateway.ignore_state_updates or "action" not in self._device.changed_keys - or self._device.action == "" ): return - state, code, _area = self._device.action.split(",") + try: + state, code, _area = self._device.action.split(",") + except (AttributeError, ValueError): + return if state not in DECONZ_TO_ALARM_STATE: return diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 798a96a43d7..418545f11cf 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -267,6 +267,50 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): # Unsupported events + # Bad action string; string is None + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"action": None}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 1 + + # Bad action string; empty string + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"action": ""}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 1 + + # Bad action string; too few "," + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"action": "armed_away,1234"}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 1 + + # Bad action string; unsupported command + event_changed_sensor = { "t": "event", "e": "changed", @@ -279,6 +323,8 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): assert len(captured_events) == 1 + # Only care for changes to action + event_changed_sensor = { "t": "event", "e": "changed", From c49decb7f07450ae528ff40ee0f7119c48307974 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Jul 2021 14:35:11 -0700 Subject: [PATCH 539/818] Convert CO2Signal to data update coordinator and add fossil fuel percentage (#53370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Martin Hjelmare Co-authored-by: Daniel Hjelseth Høyer --- .../components/co2signal/__init__.py | 135 +++++++++++++++++- .../components/co2signal/config_flow.py | 67 +-------- homeassistant/components/co2signal/sensor.py | 120 +++++++++------- homeassistant/components/co2signal/util.py | 5 +- .../components/co2signal/test_config_flow.py | 37 +++-- 5 files changed, 227 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index 50a453ac5f3..734eb9f1ae0 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -1,16 +1,53 @@ """The CO2 Signal integration.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from datetime import timedelta +import logging +from typing import TypedDict, cast -from .const import DOMAIN # noqa: F401 +import CO2Signal + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_COUNTRY_CODE, DOMAIN +from .util import get_extra_name PLATFORMS = ["sensor"] +_LOGGER = logging.getLogger(__name__) + + +class CO2SignalData(TypedDict): + """Data field.""" + + carbonIntensity: float + fossilFuelPercentage: float + + +class CO2SignalUnit(TypedDict): + """Unit field.""" + + carbonIntensity: str + + +class CO2SignalResponse(TypedDict): + """API response.""" + + status: str + countryCode: str + data: CO2SignalData + units: CO2SignalUnit async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up CO2 Signal from a config entry.""" + coordinator = CO2SignalCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -18,3 +55,95 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +class CO2SignalCoordinator(DataUpdateCoordinator[CO2SignalResponse]): + """Data update coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=15) + ) + self._entry = entry + + @property + def entry_id(self) -> str: + """Return entry ID.""" + return self._entry.entry_id + + def get_extra_name(self) -> str | None: + """Return the extra name describing the location if not home.""" + return get_extra_name(self._entry.data) + + async def _async_update_data(self) -> CO2SignalResponse: + """Fetch the latest data from the source.""" + try: + data = await self.hass.async_add_executor_job( + get_data, self.hass, self._entry.data + ) + except InvalidAuth as err: + raise ConfigEntryAuthFailed from err + except CO2Error as err: + raise UpdateFailed(str(err)) from err + + return data + + +class CO2Error(HomeAssistantError): + """Base error.""" + + +class InvalidAuth(CO2Error): + """Raised when invalid authentication credentials are provided.""" + + +class APIRatelimitExceeded(CO2Error): + """Raised when the API rate limit is exceeded.""" + + +class UnknownError(CO2Error): + """Raised when an unknown error occurs.""" + + +def get_data(hass: HomeAssistant, config: dict) -> CO2SignalResponse: + """Get data from the API.""" + if CONF_COUNTRY_CODE in config: + latitude = None + longitude = None + else: + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + try: + data = CO2Signal.get_latest( + config[CONF_API_KEY], + config.get(CONF_COUNTRY_CODE), + latitude, + longitude, + wait=False, + ) + + except ValueError as err: + err_str = str(err) + + if "Invalid authentication credentials" in err_str: + raise InvalidAuth from err + if "API rate limit exceeded." in err_str: + raise APIRatelimitExceeded from err + + _LOGGER.exception("Unexpected exception") + raise UnknownError from err + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + raise UnknownError from err + + else: + if "error" in data: + raise UnknownError(data["error"]) + + if data.get("status") != "ok": + _LOGGER.exception("Unexpected response: %s", data) + raise UnknownError + + return cast(CO2SignalResponse, data) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 953a09719ec..e3862d6347c 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -4,15 +4,14 @@ from __future__ import annotations import logging from typing import Any -import CO2Signal import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_data from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name @@ -34,62 +33,6 @@ def _get_entry_type(config: dict) -> str: return TYPE_USE_HOME -def _validate_info(hass, config: dict) -> dict: - """Validate the passed in info.""" - if CONF_COUNTRY_CODE in config: - latitude = None - longitude = None - else: - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - - try: - data = CO2Signal.get_latest( - config[CONF_API_KEY], - config.get(CONF_COUNTRY_CODE), - latitude, - longitude, - wait=False, - ) - - except ValueError as err: - err_str = str(err) - - if "Invalid authentication credentials" in err_str: - raise InvalidAuth from err - if "API rate limit exceeded." in err_str: - raise APIRatelimitExceeded from err - - _LOGGER.exception("Unexpected exception") - raise UnknownError from err - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - raise UnknownError from err - - else: - if data.get("status") != "ok": - _LOGGER.exception("Unexpected response: %s", data) - raise UnknownError - - return data - - -class CO2Error(HomeAssistantError): - """Base error.""" - - -class InvalidAuth(CO2Error): - """Raised when invalid authentication credentials are provided.""" - - -class APIRatelimitExceeded(CO2Error): - """Raised when the API rate limit is exceeded.""" - - -class UnknownError(CO2Error): - """Raised when an unknown error occurs.""" - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Co2signal.""" @@ -136,12 +79,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") try: - await self.hass.async_add_executor_job(_validate_info, self.hass, data) + await self.hass.async_add_executor_job(get_data, self.hass, data) except CO2Error: return self.async_abort(reason="unknown") return self.async_create_entry( - title=get_extra_name(self.hass, data) or "CO2 Signal", data=data + title=get_extra_name(data) or "CO2 Signal", data=data ) async def async_step_user( @@ -227,7 +170,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} try: - await self.hass.async_add_executor_job(_validate_info, self.hass, data) + await self.hass.async_add_executor_job(get_data, self.hass, data) except InvalidAuth: errors["base"] = "invalid_auth" except APIRatelimitExceeded: @@ -236,7 +179,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: return self.async_create_entry( - title=get_extra_name(self.hass, data) or "CO2 Signal", + title=get_extra_name(data) or "CO2 Signal", data=data, ) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 88a80df0b54..0b79378c36b 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,32 +1,36 @@ """Support for the CO2signal platform.""" -from datetime import timedelta -import logging +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from typing import cast -import CO2Signal import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME, - CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN, - ENERGY_KILO_WATT_HOUR, + PERCENTAGE, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, update_coordinator +from homeassistant.helpers.typing import StateType +from . import CO2SignalCoordinator, CO2SignalResponse from .const import ATTRIBUTION, CONF_COUNTRY_CODE, DOMAIN, MSG_LOCATION -from .util import get_extra_name -_LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=3) -CO2_INTENSITY_UNIT = f"CO2eq/{ENERGY_KILO_WATT_HOUR}" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, @@ -37,6 +41,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +@dataclass +class CO2SensorEntityDescription: + """Provide a description of a CO2 sensor.""" + + key: str + name: str + unit_of_measurement: str | None = None + # For backwards compat, allow description to override unique ID key to use + unique_id: str | None = None + + +SENSORS = ( + CO2SensorEntityDescription( + key="carbonIntensity", + name="CO2 intensity", + unique_id="co2intensity", + # No unit, it's extracted from response. + ), + CO2SensorEntityDescription( + key="fossilFuelPercentage", + name="Grid fossil fuel percentage", + unit_of_measurement=PERCENTAGE, + ), +) + + async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the CO2signal sensor.""" await hass.config_entries.flow.async_init( @@ -48,59 +78,47 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities): """Set up the CO2signal sensor.""" - name = "CO2 intensity" - if extra_name := get_extra_name(hass, entry.data): - name += f" - {extra_name}" - - async_add_entities( - [ - CO2Sensor( - name, - entry.data, - entry_id=entry.entry_id, - ) - ], - True, - ) + coordinator: CO2SignalCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities(CO2Sensor(coordinator, description) for description in SENSORS) -class CO2Sensor(SensorEntity): +class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorEntity): """Implementation of the CO2Signal sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT _attr_icon = "mdi:molecule-co2" - _attr_unit_of_measurement = CO2_INTENSITY_UNIT - def __init__(self, name, config, entry_id): + def __init__( + self, coordinator: CO2SignalCoordinator, description: CO2SensorEntityDescription + ) -> None: """Initialize the sensor.""" - self._config = config + super().__init__(coordinator) + self._description = description + + name = description.name + if extra_name := coordinator.get_extra_name(): + name = f"{extra_name} - {name}" + self._attr_name = name self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, entry_id)}, + ATTR_IDENTIFIERS: {(DOMAIN, coordinator.entry_id)}, ATTR_NAME: "CO2 signal", ATTR_MANUFACTURER: "Tmrow.com", "entry_type": "service", } - self._attr_unique_id = f"{entry_id}_co2intensity" - - def update(self): - """Get the latest data and updates the states.""" - _LOGGER.debug("Update data for %s", self.name) - - if CONF_COUNTRY_CODE in self._config: - kwargs = {"country_code": self._config[CONF_COUNTRY_CODE]} - elif CONF_LATITUDE in self._config: - kwargs = { - "latitude": self._config[CONF_LATITUDE], - "longitude": self._config[CONF_LONGITUDE], - } - else: - kwargs = { - "latitude": self.hass.config.latitude, - "longitude": self.hass.config.longitude, - } - - self._attr_state = round( - CO2Signal.get_latest_carbon_intensity(self._config[CONF_API_KEY], **kwargs), - 2, + self._attr_unique_id = ( + f"{coordinator.entry_id}_{description.unique_id or description.key}" ) + + @property + def state(self) -> StateType: + """Return sensor state.""" + return round(self.coordinator.data["data"][self._description.key], 2) # type: ignore[misc] + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + if self._description.unit_of_measurement: + return self._description.unit_of_measurement + return cast(str, self.coordinator.data["units"].get(self._description.key)) diff --git a/homeassistant/components/co2signal/util.py b/homeassistant/components/co2signal/util.py index 9cda6f558bf..af0bec34904 100644 --- a/homeassistant/components/co2signal/util.py +++ b/homeassistant/components/co2signal/util.py @@ -1,13 +1,14 @@ """Utils for CO2 signal.""" from __future__ import annotations +from collections.abc import Mapping + from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant from .const import CONF_COUNTRY_CODE -def get_extra_name(hass: HomeAssistant, config: dict) -> str | None: +def get_extra_name(config: Mapping) -> str | None: """Return the extra name describing the location if not home.""" if CONF_COUNTRY_CODE in config: return config[CONF_COUNTRY_CODE] diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 25d38526133..129ab7124fe 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -23,10 +23,7 @@ async def test_form_home(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ), patch( + with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -65,10 +62,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) assert result2["type"] == RESULT_TYPE_FORM - with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ), patch( + with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -109,10 +103,7 @@ async def test_form_country(hass: HomeAssistant) -> None: ) assert result2["type"] == RESULT_TYPE_FORM - with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ), patch( + with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -148,7 +139,7 @@ async def test_form_error_handling(hass: HomeAssistant, err_str, err_code) -> No ) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", side_effect=ValueError(err_str), ): result2 = await hass.config_entries.flow.async_configure( @@ -170,7 +161,7 @@ async def test_form_error_unexpected_error(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", side_effect=Exception("Boom"), ): result2 = await hass.config_entries.flow.async_configure( @@ -192,7 +183,7 @@ async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", return_value={"status": "error"}, ): result2 = await hass.config_entries.flow.async_configure( @@ -212,7 +203,7 @@ async def test_import(hass: HomeAssistant) -> None: await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", return_value=VALID_PAYLOAD, ): assert await async_setup_component( @@ -221,10 +212,18 @@ async def test_import(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.config_entries.async_entries("co2signal")) == 1 + state = hass.states.get("sensor.co2_intensity") assert state is not None assert state.state == "45.99" assert state.name == "CO2 intensity" + assert state.attributes["unit_of_measurement"] == "gCO2eq/kWh" + + state = hass.states.get("sensor.grid_fossil_fuel_percentage") + assert state is not None + assert state.state == "5.46" + assert state.name == "Grid fossil fuel percentage" + assert state.attributes["unit_of_measurement"] == "%" async def test_import_abort_existing_home(hass: HomeAssistant) -> None: @@ -233,7 +232,7 @@ async def test_import_abort_existing_home(hass: HomeAssistant) -> None: MockConfigEntry(domain="co2signal", data={"api_key": "abcd"}).add_to_hass(hass) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", return_value=VALID_PAYLOAD, ): assert await async_setup_component( @@ -252,7 +251,7 @@ async def test_import_abort_existing_country(hass: HomeAssistant) -> None: ).add_to_hass(hass) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", return_value=VALID_PAYLOAD, ): assert await async_setup_component( @@ -279,7 +278,7 @@ async def test_import_abort_existing_coordinates(hass: HomeAssistant) -> None: ).add_to_hass(hass) with patch( - "homeassistant.components.co2signal.config_flow.CO2Signal.get_latest", + "CO2Signal.get_latest", return_value=VALID_PAYLOAD, ): assert await async_setup_component( From 7fa8586b17bcac01f9979ae3a818ed28269d0714 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 24 Jul 2021 00:08:57 +0000 Subject: [PATCH 540/818] [ci skip] Translation update --- .../components/adax/translations/pl.json | 20 +++++++++++ .../airvisual/translations/sensor.pl.json | 20 +++++++++++ .../components/automate/translations/de.json | 19 +++++++++++ .../components/automate/translations/nl.json | 19 +++++++++++ .../components/automate/translations/pl.json | 19 +++++++++++ .../components/co2signal/translations/pl.json | 34 +++++++++++++++++++ .../components/coinbase/translations/pl.json | 1 + .../components/flipr/translations/nl.json | 30 ++++++++++++++++ .../components/flipr/translations/pl.json | 30 ++++++++++++++++ .../growatt_server/translations/pl.json | 1 + .../components/homekit/translations/pl.json | 2 +- .../components/honeywell/translations/pl.json | 17 ++++++++++ .../nfandroidtv/translations/pl.json | 21 ++++++++++++ .../components/samsungtv/translations/cs.json | 1 + .../switcher_kis/translations/pl.json | 13 +++++++ .../synology_dsm/translations/nl.json | 11 +++++- .../synology_dsm/translations/pl.json | 11 +++++- .../components/zwave_js/translations/pl.json | 8 +++++ 18 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/adax/translations/pl.json create mode 100644 homeassistant/components/airvisual/translations/sensor.pl.json create mode 100644 homeassistant/components/automate/translations/de.json create mode 100644 homeassistant/components/automate/translations/nl.json create mode 100644 homeassistant/components/automate/translations/pl.json create mode 100644 homeassistant/components/co2signal/translations/pl.json create mode 100644 homeassistant/components/flipr/translations/nl.json create mode 100644 homeassistant/components/flipr/translations/pl.json create mode 100644 homeassistant/components/honeywell/translations/pl.json create mode 100644 homeassistant/components/nfandroidtv/translations/pl.json create mode 100644 homeassistant/components/switcher_kis/translations/pl.json diff --git a/homeassistant/components/adax/translations/pl.json b/homeassistant/components/adax/translations/pl.json new file mode 100644 index 00000000000..05d2f4a918c --- /dev/null +++ b/homeassistant/components/adax/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "account_id": "Identyfikator konta", + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.pl.json b/homeassistant/components/airvisual/translations/sensor.pl.json new file mode 100644 index 00000000000..48835f36f69 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.pl.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Tlenek w\u0119gla", + "n2": "Dwutlenek azotu", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Dwutlenek siarki" + }, + "airvisual__pollutant_level": { + "good": "dobry", + "hazardous": "niebezpieczny", + "moderate": "umiarkowany", + "unhealthy": "niezdrowy", + "unhealthy_sensitive": "niezdrowy dla grup wra\u017cliwych", + "very_unhealthy": "bardzo niezdrowy" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/de.json b/homeassistant/components/automate/translations/de.json new file mode 100644 index 00000000000..fa773dbf708 --- /dev/null +++ b/homeassistant/components/automate/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/nl.json b/homeassistant/components/automate/translations/nl.json new file mode 100644 index 00000000000..bb92bf9e593 --- /dev/null +++ b/homeassistant/components/automate/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/pl.json b/homeassistant/components/automate/translations/pl.json new file mode 100644 index 00000000000..647c0921e3a --- /dev/null +++ b/homeassistant/components/automate/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/pl.json b/homeassistant/components/co2signal/translations/pl.json new file mode 100644 index 00000000000..3b243649180 --- /dev/null +++ b/homeassistant/components/co2signal/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "api_ratelimit": "Przekroczono limit interfejsu API", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "api_ratelimit": "Przekroczono limit interfejsu API", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + } + }, + "country": { + "data": { + "country_code": "Kod kraju" + } + }, + "user": { + "data": { + "api_key": "Token dost\u0119pu", + "location": "Pobierz dane dla" + }, + "description": "Odwied\u017a https://co2signal.com/, aby uzyska\u0107 token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index a5918556892..8c269432b31 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Salda portfela do zg\u0142oszenia.", + "exchange_base": "Waluta bazowa dla czujnik\u00f3w kurs\u00f3w walut.", "exchange_rate_currencies": "Kursy walut do zg\u0142oszenia." }, "description": "Dostosuj opcje Coinbase" diff --git a/homeassistant/components/flipr/translations/nl.json b/homeassistant/components/flipr/translations/nl.json new file mode 100644 index 00000000000..d66028ee244 --- /dev/null +++ b/homeassistant/components/flipr/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "no_flipr_id_found": "Er is nog geen flipr id aan uw account gekoppeld. U moet eerst controleren of het werkt met de mobiele app van Flipr.", + "unknown": "Onverwachte fout" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Kies uw Flipr-ID in de lijst", + "title": "Kies uw Flipr" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + }, + "description": "Maak verbinding met uw Flipr-account.", + "title": "Maak verbinding met Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/pl.json b/homeassistant/components/flipr/translations/pl.json new file mode 100644 index 00000000000..1095c33a83e --- /dev/null +++ b/homeassistant/components/flipr/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_flipr_id_found": "Brak identyfikatora flipr powi\u0105zanego z Twoim kontem. Sprawd\u017a najpierw, czy dzia\u0142a z aplikacj\u0105 mobiln\u0105 Flipr.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Identyfikator Flipr" + }, + "description": "Wybierz sw\u00f3j identyfikator Flipr z listy", + "title": "Wyb\u00f3r identyfikatora Flipr" + }, + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + }, + "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta Flipr.", + "title": "Po\u0142\u0105czenie z Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/pl.json b/homeassistant/components/growatt_server/translations/pl.json index 2041a577489..01e37307d7f 100644 --- a/homeassistant/components/growatt_server/translations/pl.json +++ b/homeassistant/components/growatt_server/translations/pl.json @@ -17,6 +17,7 @@ "data": { "name": "Nazwa", "password": "Has\u0142o", + "url": "URL", "username": "Nazwa u\u017cytkownika" }, "title": "Wprowad\u017a dane Growatt." diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 2cd687ebc47..15ccb10e118 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domeny do uwzgl\u0119dnienia" }, - "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera oraz kamery.", + "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera, pilota na bazie aktywno\u015bci, zamka oraz kamery.", "title": "Wybierz uwzgl\u0119dniane domeny" } } diff --git a/homeassistant/components/honeywell/translations/pl.json b/homeassistant/components/honeywell/translations/pl.json new file mode 100644 index 00000000000..c109565e33a --- /dev/null +++ b/homeassistant/components/honeywell/translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce u\u017cywane na mytotalconnectcomfort.com", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/pl.json b/homeassistant/components/nfandroidtv/translations/pl.json new file mode 100644 index 00000000000..597d8bb9200 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa" + }, + "description": "Ta integracja wymaga aplikacji Powiadomienia dla Androida TV. \n\nAndroid TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nNale\u017cy skonfigurowa\u0107 rezerwacj\u0119 DHCP na routerze (patrz instrukcja obs\u0142ugi routera) lub statyczny adres IP na urz\u0105dzeniu. Je\u015bli tego nie zrobisz, urz\u0105dzenie ostatecznie stanie si\u0119 niedost\u0119pne.", + "title": "Powiadomienia dla Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json index 3bfc07a5fe1..d45c9247b4e 100644 --- a/homeassistant/components/samsungtv/translations/cs.json +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -5,6 +5,7 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "auth_missing": "Home Assistant nem\u00e1 opr\u00e1vn\u011bn\u00ed k p\u0159ipojen\u00ed k t\u00e9to televizi Samsung. Zkontrolujte nastaven\u00ed sv\u00e9 televize a povolte Home Assistant.", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "id_missing": "Toto za\u0159\u00edzen\u00ed Samsung nem\u00e1 s\u00e9riov\u00e9 \u010d\u00edslo.", "not_supported": "Tato televize Samsung nen\u00ed aktu\u00e1ln\u011b podporov\u00e1na.", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" diff --git a/homeassistant/components/switcher_kis/translations/pl.json b/homeassistant/components/switcher_kis/translations/pl.json new file mode 100644 index 00000000000..a8ee3fa57ac --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index 6fa342800a2..6ce9f1b63b9 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -29,6 +30,14 @@ "description": "Wil je {name} ({host}) instellen?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Reden: {details}", + "title": "Synology DSM Verifieer de integratie opnieuw" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index e060f666f3c..2979aa2c416 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -29,6 +30,14 @@ "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Pow\u00f3d: {details}", + "title": "Ponownie uwierzytelnij integracj\u0119 Synology DSM" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 6d6141e1238..cc000691d25 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -56,6 +56,14 @@ "config_parameter": "Warto\u015b\u0107 parametru jest {subtype}", "node_status": "Stan w\u0119z\u0142a", "value": "Aktualna warto\u015b\u0107 warto\u015bci Z-Wave" + }, + "trigger_type": { + "event.notification.entry_control": "Wys\u0142ano powiadomienie kontroli wpisu", + "event.notification.notification": "Wys\u0142ano powiadomienie", + "event.value_notification.basic": "Podstawowe wydarzenie CC na {subtype}", + "event.value_notification.central_scene": "Akcja sceny centralnej na {subtype}", + "event.value_notification.scene_activation": "Aktywacja sceny na {subtype}", + "state.node_status": "Zmieni\u0142 si\u0119 stan w\u0119z\u0142a" } }, "options": { From bf3a16eed41cd79f2b3403c36f495abc392d8136 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 24 Jul 2021 06:48:32 +0200 Subject: [PATCH 541/818] Use class attributes in devolo Home Control (#53360) --- .../devolo_home_control/binary_sensor.py | 28 +++---- .../components/devolo_home_control/climate.py | 64 ++++++---------- .../components/devolo_home_control/cover.py | 28 ++++--- .../devolo_home_control/devolo_device.py | 76 ++++++------------- .../components/devolo_home_control/light.py | 6 +- .../components/devolo_home_control/sensor.py | 44 +++++------ .../components/devolo_home_control/switch.py | 15 ++-- 7 files changed, 94 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index c8ce1c3585d..c19d74b4c33 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -81,33 +81,28 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): element_uid=element_uid, ) - self._device_class = DEVICE_CLASS_MAPPING.get( + self._attr_device_class = DEVICE_CLASS_MAPPING.get( self._binary_sensor_property.sub_type or self._binary_sensor_property.sensor_type ) - if self._device_class is None: + if self._attr_device_class is None: if device_instance.binary_sensor_property.get(element_uid).sub_type != "": - self._name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}" + self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}" else: - self._name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}" + self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}" self._value = self._binary_sensor_property.state if element_uid.startswith("devolo.WarningBinaryFI:"): - self._device_class = DEVICE_CLASS_PROBLEM - self._enabled_default = False + self._attr_device_class = DEVICE_CLASS_PROBLEM + self._attr_entity_registry_enabled_default = False @property def is_on(self) -> bool: """Return the state.""" return bool(self._value) - @property - def device_class(self) -> str | None: - """Return device class.""" - return self._device_class - class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): """Representation of a remote control within devolo Home Control.""" @@ -131,12 +126,7 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): ) self._key = key - self._state = False - - @property - def is_on(self) -> bool: - """Return the state.""" - return self._state + self._attr_is_on = False def _sync(self, message: tuple) -> None: """Update the binary sensor state.""" @@ -144,11 +134,11 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): message[0] == self._remote_control_property.element_uid and message[1] == self._key ): - self._state = True + self._attr_is_on = True elif ( message[0] == self._remote_control_property.element_uid and message[1] == 0 ): - self._state = False + self._attr_is_on = False else: self._generic_message(message) self.schedule_update_ha_state() diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 6b890544da5..ff4d8a01198 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -3,6 +3,9 @@ from __future__ import annotations from typing import Any +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from homeassistant.components.climate import ( ATTR_TEMPERATURE, HVAC_MODE_HEAT, @@ -47,6 +50,25 @@ async def async_setup_entry( class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntity): """Representation of a climate/thermostat device within devolo Home Control.""" + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: + """Initialize a climate entity within devolo Home Control.""" + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, + ) + + self._attr_hvac_mode = HVAC_MODE_HEAT + self._attr_hvac_modes = [HVAC_MODE_HEAT] + self._attr_min_temp = self._multi_level_switch_property.min + self._attr_max_temp = self._multi_level_switch_property.max + self._attr_precision = PRECISION_TENTHS + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + self._attr_target_temperature_step = PRECISION_HALVES + self._attr_temperature_unit = TEMP_CELSIUS + @property def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -67,48 +89,6 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit """Return the target temperature.""" return self._value - @property - def target_temperature_step(self) -> float: - """Return the precision of the target temperature.""" - return PRECISION_HALVES - - @property - def hvac_mode(self) -> str: - """Return the supported HVAC mode.""" - return HVAC_MODE_HEAT - - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT] - - @property - def min_temp(self) -> float: - """Return the minimum set temperature value.""" - min_temp: float = self._multi_level_switch_property.min - return min_temp - - @property - def max_temp(self) -> float: - """Return the maximum set temperature value.""" - max_temp: float = self._multi_level_switch_property.max - return max_temp - - @property - def precision(self) -> float: - """Return the precision of the set temperature.""" - return PRECISION_TENTHS - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_TARGET_TEMPERATURE - - @property - def temperature_unit(self) -> str: - """Return the supported unit of temperature.""" - return TEMP_CELSIUS - def set_hvac_mode(self, hvac_mode: str) -> None: """Do nothing as devolo devices do not support changing the hvac mode.""" diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index b2ea2f66b67..7a1a93596d3 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -3,6 +3,9 @@ from __future__ import annotations from typing import Any +from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.homecontrol import HomeControl + from homeassistant.components.cover import ( DEVICE_CLASS_BLIND, SUPPORT_CLOSE, @@ -42,26 +45,31 @@ async def async_setup_entry( class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity): """Representation of a cover device within devolo Home Control.""" + def __init__( + self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str + ) -> None: + """Initialize a climate entity within devolo Home Control.""" + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, + ) + + self._attr_device_class = DEVICE_CLASS_BLIND + self._attr_supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + ) + @property def current_cover_position(self) -> int: """Return the current position. 0 is closed. 100 is open.""" return self._value - @property - def device_class(self) -> str: - """Return the class of the device.""" - return DEVICE_CLASS_BLIND - @property def is_closed(self) -> bool: """Return if the blind is closed or not.""" return not bool(self._value) - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - def open_cover(self, **kwargs: Any) -> None: """Open the blind.""" self._multi_level_switch_property.set(100) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index e8eec3ca7dc..781799cbf37 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -6,7 +6,7 @@ import logging from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DOMAIN from .subscriber import Subscriber @@ -22,30 +22,34 @@ class DevoloDeviceEntity(Entity): ) -> None: """Initialize a devolo device entity.""" self._device_instance = device_instance - self._unique_id = element_uid self._homecontrol = homecontrol - self._name: str = device_instance.settings_property[ + + self._attr_available = ( + device_instance.is_online() + ) # This is not doing I/O. It fetches an internal state of the API + self._attr_name: str = device_instance.settings_property[ "general_device_settings" ].name - self._area = device_instance.settings_property["general_device_settings"].zone - self._device_class: str | None = None - self._value: int - self._unit = "" - self._enabled_default = True - - # This is not doing I/O. It fetches an internal state of the API - self._available: bool = device_instance.is_online() - - # Get the brand and model information - self._brand = device_instance.brand - self._model = device_instance.name + self._attr_should_poll = False + self._attr_unique_id = element_uid + self._attr_device_info = { + "identifiers": {(DOMAIN, self._device_instance.uid)}, + "name": self._attr_name, + "manufacturer": device_instance.brand, + "model": device_instance.name, + "suggested_area": device_instance.settings_property[ + "general_device_settings" + ].zone, + } self.subscriber: Subscriber | None = None self.sync_callback = self._sync + self._value: int + self._unit = "" async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" - self.subscriber = Subscriber(self._name, callback=self.sync_callback) + self.subscriber = Subscriber(self._attr_name, callback=self.sync_callback) self._homecontrol.publisher.register( self._device_instance.uid, self.subscriber, self.sync_callback ) @@ -56,45 +60,9 @@ class DevoloDeviceEntity(Entity): self._device_instance.uid, self.subscriber ) - @property - def unique_id(self) -> str: - """Return the unique ID of the entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._device_instance.uid)}, - "name": self._name, - "manufacturer": self._brand, - "model": self._model, - "suggested_area": self._area, - } - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def should_poll(self) -> bool: - """Return the polling state.""" - return False - - @property - def name(self) -> str: - """Return the display name of this entity.""" - return self._name - - @property - def available(self) -> bool: - """Return the online state.""" - return self._available - def _sync(self, message: tuple) -> None: """Update the state.""" - if message[0] == self._unique_id: + if message[0] == self._attr_unique_id: self._value = message[1] else: self._generic_message(message) @@ -106,6 +74,6 @@ class DevoloDeviceEntity(Entity): self._value = message[1] elif len(message) == 3 and message[2] == "status": # Maybe the API wants to tell us, that the device went on- or offline. - self._available = self._device_instance.is_online() + self._attr_available = self._device_instance.is_online() else: _LOGGER.debug("No valid message received: %s", message) diff --git a/homeassistant/components/devolo_home_control/light.py b/homeassistant/components/devolo_home_control/light.py index 27c637cf114..28da95c8902 100644 --- a/homeassistant/components/devolo_home_control/light.py +++ b/homeassistant/components/devolo_home_control/light.py @@ -53,6 +53,7 @@ class DevoloLightDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, LightEntity): element_uid=element_uid, ) + self._attr_supported_features = SUPPORT_BRIGHTNESS self._binary_switch_property = device_instance.binary_switch_property.get( element_uid.replace("Dimmer", "BinarySwitch") ) @@ -67,11 +68,6 @@ class DevoloLightDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, LightEntity): """Return the state of the light.""" return bool(self._value) - @property - def supported_features(self) -> int: - """Return the supported features.""" - return SUPPORT_BRIGHTNESS - def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if kwargs.get(ATTR_BRIGHTNESS) is not None: diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index af67c6cd78a..7cb8cc8e837 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -77,21 +77,11 @@ async def async_setup_entry( class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity): """Abstract representation of a multi level sensor within devolo Home Control.""" - @property - def device_class(self) -> str | None: - """Return device class.""" - return self._device_class - @property def state(self) -> int: """Return the state of the sensor.""" return self._value - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return self._unit - class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): """Representation of a generic multi level sensor within devolo Home Control.""" @@ -113,18 +103,18 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): element_uid=element_uid, ) - self._device_class = DEVICE_CLASS_MAPPING.get( + self._attr_device_class = DEVICE_CLASS_MAPPING.get( self._multi_level_sensor_property.sensor_type ) + self._attr_unit_of_measurement = self._multi_level_sensor_property.unit self._value = self._multi_level_sensor_property.value - self._unit = self._multi_level_sensor_property.unit - if self._device_class is None: - self._name += f" {self._multi_level_sensor_property.sensor_type}" + if self._attr_device_class is None: + self._attr_name += f" {self._multi_level_sensor_property.sensor_type}" if element_uid.startswith("devolo.VoltageMultiLevelSensor:"): - self._enabled_default = False + self._attr_entity_registry_enabled_default = False class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): @@ -141,10 +131,10 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): element_uid=element_uid, ) - self._device_class = DEVICE_CLASS_MAPPING.get("battery") + self._attr_device_class = DEVICE_CLASS_MAPPING.get("battery") + self._attr_unit_of_measurement = PERCENTAGE self._value = device_instance.battery_level - self._unit = PERCENTAGE class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): @@ -166,7 +156,10 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): ) self._sensor_type = consumption - self._device_class = DEVICE_CLASS_MAPPING.get(consumption) + self._attr_device_class = DEVICE_CLASS_MAPPING.get(consumption) + self._attr_unit_of_measurement = getattr( + device_instance.consumption_property[element_uid], f"{consumption}_unit" + ) if consumption == "total": self._attr_state_class = STATE_CLASS_MEASUREMENT @@ -177,27 +170,24 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): self._value = getattr( device_instance.consumption_property[element_uid], consumption ) - self._unit = getattr( - device_instance.consumption_property[element_uid], f"{consumption}_unit" - ) - self._name += f" {consumption}" + self._attr_name += f" {consumption}" @property def unique_id(self) -> str: """Return the unique ID of the entity.""" - return f"{self._unique_id}_{self._sensor_type}" + return f"{self._attr_unique_id}_{self._sensor_type}" def _sync(self, message: tuple) -> None: """Update the consumption sensor state.""" - if message[0] == self._unique_id and message[2] != "total_since": + if message[0] == self._attr_unique_id and message[2] != "total_since": self._value = getattr( - self._device_instance.consumption_property[self._unique_id], + self._device_instance.consumption_property[self._attr_unique_id], self._sensor_type, ) - elif message[0] == self._unique_id and message[2] == "total_since": + elif message[0] == self._attr_unique_id and message[2] == "total_since": self._attr_last_reset = self._device_instance.consumption_property[ - self._unique_id + self._attr_unique_id ].total_since else: self._generic_message(message) diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index c9dabf23c39..4896d66b805 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -51,29 +51,24 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): element_uid=element_uid, ) self._binary_switch_property = self._device_instance.binary_switch_property.get( - self._unique_id + self._attr_unique_id ) - self._is_on: bool = self._binary_switch_property.state - - @property - def is_on(self) -> bool: - """Return the state.""" - return self._is_on + self._attr_is_on = self._binary_switch_property.state def turn_on(self, **kwargs: Any) -> None: """Switch on the device.""" - self._is_on = True self._binary_switch_property.set(state=True) def turn_off(self, **kwargs: Any) -> None: """Switch off the device.""" - self._is_on = False self._binary_switch_property.set(state=False) def _sync(self, message: tuple) -> None: """Update the binary switch state and consumption.""" if message[0].startswith("devolo.BinarySwitch"): - self._is_on = self._device_instance.binary_switch_property[message[0]].state + self._attr_is_on = self._device_instance.binary_switch_property[ + message[0] + ].state else: self._generic_message(message) self.schedule_update_ha_state() From 0db160e37235351877fccb2b39f91566ef58988a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 24 Jul 2021 07:03:44 +0100 Subject: [PATCH 542/818] Handle homekit accessories where the pairing flag is wrong (#53385) --- .../homekit_controller/config_flow.py | 25 ++++++++- .../homekit_controller/test_config_flow.py | 56 +++++++++++++++++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index e8357a4001d..a19c1d0c107 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -3,6 +3,7 @@ import logging import re import aiohomekit +from aiohomekit.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries @@ -270,7 +271,29 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # invalid. Remove it automatically. existing = find_existing_host(self.hass, hkid) if not paired and existing: - await self.hass.config_entries.async_remove(existing.entry_id) + if self.controller is None: + await self._async_setup_controller() + + pairing = self.controller.load_pairing( + existing.data["AccessoryPairingID"], dict(existing.data) + ) + try: + await pairing.list_accessories_and_characteristics() + _LOGGER.debug( + "%s (%s - %s) claims to be unpaired but isn't. It's implementation of HomeKit is defective or a zeroconf relay is broadcasting stale data", + name, + model, + hkid, + ) + return self.async_abort(reason="already_paired") + except AuthenticationError: + _LOGGER.debug( + "%s (%s - %s) is unpaired. Removing invalid pairing for this device", + name, + model, + hkid, + ) + await self.hass.config_entries.async_remove(existing.entry_id) # Set unique-id and error out if it's already configured self._abort_if_unique_id_configured(updates=updated_ip_port) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 52685334500..b08659bf77b 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -4,6 +4,7 @@ import unittest.mock from unittest.mock import AsyncMock, patch import aiohomekit +from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -351,8 +352,48 @@ async def test_discovery_does_not_ignore_non_homekit(hass, controller): assert result["type"] == "form" +async def test_discovery_broken_pairing_flag(hass, controller): + """ + There is already a config entry for the pairing and its pairing flag is wrong in zeroconf. + + We have seen this particular implementation error in 2 different devices. + """ + await controller.add_paired_device(Accessories(), "00:00:00:00:00:00") + + MockConfigEntry( + domain="homekit_controller", + data={"AccessoryPairingID": "00:00:00:00:00:00"}, + unique_id="00:00:00:00:00:00", + ).add_to_hass(hass) + + # We just added a mock config entry so it must be visible in hass + assert len(hass.config_entries.async_entries()) == 1 + + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Make sure that we are pairable + assert discovery_info["properties"]["sf"] != 0x0 + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + + # Should still be paired. + config_entry_count = len(hass.config_entries.async_entries()) + assert config_entry_count == 1 + + # Even though discovered as pairable, we bail out as already paired. + assert result["reason"] == "already_paired" + + async def test_discovery_invalid_config_entry(hass, controller): """There is already a config entry for the pairing id but it's invalid.""" + pairing = await controller.add_paired_device(Accessories(), "00:00:00:00:00:00") + MockConfigEntry( domain="homekit_controller", data={"AccessoryPairingID": "00:00:00:00:00:00"}, @@ -366,11 +407,16 @@ async def test_discovery_invalid_config_entry(hass, controller): discovery_info = get_device_discovery_info(device) # Device is discovered - result = await hass.config_entries.flow.async_init( - "homekit_controller", - context={"source": config_entries.SOURCE_ZEROCONF}, - data=discovery_info, - ) + with patch.object( + pairing, + "list_accessories_and_characteristics", + side_effect=AuthenticationError("Invalid pairing keys"), + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) # Discovery of a HKID that is in a pairable state but for which there is # already a config entry - in that case the stale config entry is From 4b393f215dbd206eea8324a1c1fe225dac678f60 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 24 Jul 2021 05:55:54 -0400 Subject: [PATCH 543/818] Use entity class attributes for asuswrt (#52685) * Use entity class attributes for asuswrt * fix * tweak --- .../components/asuswrt/device_tracker.py | 53 +++++-------------- homeassistant/components/asuswrt/sensor.py | 41 +++----------- 2 files changed, 22 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index a0c7ec0e27a..0b5d81e3de9 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,15 +1,12 @@ """Support for ASUSWRT routers.""" from __future__ import annotations -from typing import Any - from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter @@ -55,20 +52,14 @@ def add_entities(router, async_add_entities, tracked): class AsusWrtDevice(ScannerEntity): """Representation of a AsusWrt device.""" + _attr_should_poll = False + def __init__(self, router: AsusWrtRouter, device) -> None: """Initialize a AsusWrt device.""" self._router = router self._device = device - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._device.mac - - @property - def name(self) -> str: - """Return the name.""" - return self._device.name or DEFAULT_DEVICE_NAME + self._attr_unique_id = device.mac + self._attr_name = device.name or DEFAULT_DEVICE_NAME @property def is_connected(self): @@ -80,16 +71,6 @@ class AsusWrtDevice(ScannerEntity): """Return the source type.""" return SOURCE_TYPE_ROUTER - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the attributes.""" - attrs = {} - if self._device.last_activity: - attrs["last_time_reachable"] = self._device.last_activity.isoformat( - timespec="seconds" - ) - return attrs - @property def hostname(self) -> str: """Return the hostname of device.""" @@ -105,26 +86,20 @@ class AsusWrtDevice(ScannerEntity): """Return the mac address of the device.""" return self._device.mac - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - data = { - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, - } - if self._device.name: - data["default_name"] = self._device.name - - return data - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @callback def async_on_demand_update(self): """Update state.""" self._device = self._router.devices[self._device.mac] + self._attr_device_info = { + "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, + } + if self._device.name: + self._attr_device_info["default_name"] = self._device.name + self._attr_extra_state_attributes = {} + if self._device.last_activity: + self._attr_extra_state_attributes[ + "last_time_reachable" + ] = self._device.last_activity.isoformat(timespec="seconds") self.async_write_ha_state() async def async_added_to_hass(self): diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 086c7373a4e..679ae832394 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -120,15 +120,15 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self._router = router self._sensor_type = sensor_type - self._sensor_def = sensor_def - self._name = f"{DEFAULT_PREFIX} {sensor_def[SENSOR_NAME]}" - self._unique_id = f"{DOMAIN} {self._name}" + self._attr_name = f"{DEFAULT_PREFIX} {sensor_def[SENSOR_NAME]}" self._factor = sensor_def.get(SENSOR_FACTOR) - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._sensor_def.get(SENSOR_DEFAULT_ENABLED, False) + self._attr_unique_id = f"{DOMAIN} {self.name}" + self._attr_entity_registry_enabled_default = sensor_def.get( + SENSOR_DEFAULT_ENABLED, False + ) + self._attr_unit_of_measurement = sensor_def.get(SENSOR_UNIT) + self._attr_icon = sensor_def.get(SENSOR_ICON) + self._attr_device_class = sensor_def.get(SENSOR_DEVICE_CLASS) @property def state(self) -> str: @@ -140,31 +140,6 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): return round(state / self._factor, 2) return state - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def unit_of_measurement(self) -> str: - """Return the unit.""" - return self._sensor_def.get(SENSOR_UNIT) - - @property - def icon(self) -> str: - """Return the icon.""" - return self._sensor_def.get(SENSOR_ICON) - - @property - def device_class(self) -> str: - """Return the device_class.""" - return self._sensor_def.get(SENSOR_DEVICE_CLASS) - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" From ffa7962a37464cbad6c49fd50d157a279279d71f Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 24 Jul 2021 03:12:27 -0700 Subject: [PATCH 544/818] Fix motionEye switch refresh bug (#53413) --- homeassistant/components/motioneye/switch.py | 8 +++++- tests/components/motioneye/test_switch.py | 26 +++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 2f5d4e5c2b0..08d58a9f6a1 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -20,7 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import MotionEyeEntity, listen_for_new_cameras +from . import MotionEyeEntity, get_camera_from_cameras, listen_for_new_cameras from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_SWITCH_BASE MOTIONEYE_SWITCHES = [ @@ -118,3 +118,9 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self._async_send_set_camera(False) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) + super()._handle_coordinator_update() diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py index f406fe212b7..05de2f0bbcf 100644 --- a/tests/components/motioneye/test_switch.py +++ b/tests/components/motioneye/test_switch.py @@ -24,6 +24,7 @@ import homeassistant.util.dt as dt_util from . import ( TEST_CAMERA, TEST_CAMERA_ID, + TEST_CAMERAS, TEST_SWITCH_ENTITY_ID_BASE, TEST_SWITCH_MOTION_DETECTION_ENTITY_ID, create_mock_motioneye_client, @@ -38,7 +39,7 @@ async def test_switch_turn_on_off(hass: HomeAssistant) -> None: client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) - # Verify switch is on (as per TEST_COMPONENTS above). + # Verify switch is on. entity_state = hass.states.get(TEST_SWITCH_MOTION_DETECTION_ENTITY_ID) assert entity_state assert entity_state.state == "on" @@ -93,6 +94,29 @@ async def test_switch_turn_on_off(hass: HomeAssistant) -> None: assert entity_state.state == "on" +async def test_switch_state_update_from_coordinator(hass: HomeAssistant) -> None: + """Test that coordinator data impacts state.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + # Verify switch is on. + entity_state = hass.states.get(TEST_SWITCH_MOTION_DETECTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + + updated_cameras = copy.deepcopy(TEST_CAMERAS) + updated_cameras["cameras"][0][KEY_MOTION_DETECTION] = False + client.async_get_cameras = AsyncMock(return_value=updated_cameras) + + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + # Verify the switch turns off. + entity_state = hass.states.get(TEST_SWITCH_MOTION_DETECTION_ENTITY_ID) + assert entity_state + assert entity_state.state == "off" + + async def test_switch_has_correct_entities(hass: HomeAssistant) -> None: """Test that the correct switch entities are created.""" client = create_mock_motioneye_client() From 0f78004edea0424c2baafa9b30050f02d415fd70 Mon Sep 17 00:00:00 2001 From: Samuel Tardieu Date: Sat, 24 Jul 2021 12:13:21 +0200 Subject: [PATCH 545/818] Add missing string interpolation (#53422) --- homeassistant/components/zha/core/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index c6166419e39..9e8a8450ec1 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -503,7 +503,7 @@ class ZHADevice(LogMixin): names.append( { ATTR_NAME: f"unknown {endpoint.device_type} device_type " - "of 0x{endpoint.profile_id:04x} profile id" + f"of 0x{endpoint.profile_id:04x} profile id" } ) device_info[ATTR_ENDPOINT_NAMES] = names From 72a3860361c4c678fa272a4b0186a39f725b942f Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Sat, 24 Jul 2021 03:43:10 -0700 Subject: [PATCH 546/818] Add transition to LiteJet (#47657) --- .../components/litejet/config_flow.py | 39 ++++++++++++- homeassistant/components/litejet/const.py | 2 + homeassistant/components/litejet/light.py | 41 ++++++++++---- homeassistant/components/litejet/strings.json | 10 ++++ .../components/litejet/translations/en.json | 10 ++++ tests/components/litejet/test_config_flow.py | 23 +++++++- tests/components/litejet/test_light.py | 56 +++++++++++++++++-- 7 files changed, 163 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index 2e63c150e41..4f8128bd6dc 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -10,13 +10,44 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv -from .const import DOMAIN +from .const import CONF_DEFAULT_TRANSITION, DOMAIN _LOGGER = logging.getLogger(__name__) +class LiteJetOptionsFlow(config_entries.OptionsFlow): + """Handle LiteJet options.""" + + def __init__(self, config_entry): + """Initialize LiteJet options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Manage LiteJet options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_DEFAULT_TRANSITION, + default=self.config_entry.options.get( + CONF_DEFAULT_TRANSITION, 0 + ), + ): cv.positive_int, + } + ), + ) + + class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """LiteJet config flow.""" @@ -54,3 +85,9 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import litejet config from configuration.yaml.""" return self.async_create_entry(title=import_data[CONF_PORT], data=import_data) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return LiteJetOptionsFlow(config_entry) diff --git a/homeassistant/components/litejet/const.py b/homeassistant/components/litejet/const.py index 8e27aa3a0a7..82521092106 100644 --- a/homeassistant/components/litejet/const.py +++ b/homeassistant/components/litejet/const.py @@ -6,3 +6,5 @@ CONF_EXCLUDE_NAMES = "exclude_names" CONF_INCLUDE_SWITCHES = "include_switches" PLATFORMS = ["light", "switch", "scene"] + +CONF_DEFAULT_TRANSITION = "default_transition" diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 5248afb4dbd..172e46c441a 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -3,11 +3,13 @@ import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, + SUPPORT_TRANSITION, LightEntity, ) -from .const import DOMAIN +from .const import CONF_DEFAULT_TRANSITION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,7 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for i in system.loads(): name = system.get_load_name(i) - entities.append(LiteJetLight(config_entry.entry_id, system, i, name)) + entities.append(LiteJetLight(config_entry, system, i, name)) return entities async_add_entities(await hass.async_add_executor_job(get_entities, system), True) @@ -32,9 +34,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" - def __init__(self, entry_id, lj, i, name): + def __init__(self, config_entry, lj, i, name): """Initialize a LiteJet light.""" - self._entry_id = entry_id + self._config_entry = config_entry self._lj = lj self._index = i self._brightness = 0 @@ -57,7 +59,7 @@ class LiteJetLight(LightEntity): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @property def name(self): @@ -67,7 +69,7 @@ class LiteJetLight(LightEntity): @property def unique_id(self): """Return a unique identifier for this light.""" - return f"{self._entry_id}_{self._index}" + return f"{self._config_entry.entry_id}_{self._index}" @property def brightness(self): @@ -91,16 +93,33 @@ class LiteJetLight(LightEntity): def turn_on(self, **kwargs): """Turn on the light.""" - if ATTR_BRIGHTNESS in kwargs: - brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 99) - self._lj.activate_load_at(self._index, brightness, 0) - else: + + # If neither attribute is specified then the simple activate load + # LiteJet API will use the per-light default brightness and + # transition values programmed in the LiteJet system. + if ATTR_BRIGHTNESS not in kwargs and ATTR_TRANSITION not in kwargs: self._lj.activate_load(self._index) + return + + # If either attribute is specified then Home Assistant must + # control both values. + default_transition = self._config_entry.options.get(CONF_DEFAULT_TRANSITION, 0) + transition = kwargs.get(ATTR_TRANSITION, default_transition) + brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255) / 255 * 99) + + self._lj.activate_load_at(self._index, brightness, int(transition)) def turn_off(self, **kwargs): """Turn off the light.""" + if ATTR_TRANSITION in kwargs: + self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) + return + + # If transition attribute is not specified then the simple + # deactivate load LiteJet API will use the per-light default + # transition value programmed in the LiteJet system. self._lj.deactivate_load(self._index) def update(self): """Retrieve the light's brightness from the LiteJet system.""" - self._brightness = self._lj.get_load_level(self._index) / 99 * 255 + self._brightness = int(self._lj.get_load_level(self._index) / 99 * 255) diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json index 79c4ed5f329..426dcd374af 100644 --- a/homeassistant/components/litejet/strings.json +++ b/homeassistant/components/litejet/strings.json @@ -15,5 +15,15 @@ "error": { "open_failed": "Cannot open the specified serial port." } + }, + "options": { + "step": { + "init": { + "title": "Configure LiteJet", + "data": { + "default_transition": "Default Transition (seconds)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/en.json b/homeassistant/components/litejet/translations/en.json index e09b20dc9f2..146aad276c4 100644 --- a/homeassistant/components/litejet/translations/en.json +++ b/homeassistant/components/litejet/translations/en.json @@ -15,5 +15,15 @@ "title": "Connect To LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Default Transition (seconds)" + }, + "title": "Configure LiteJet" + } + } } } \ No newline at end of file diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index 1d72324f484..cfae178f792 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries -from homeassistant.components.litejet.const import DOMAIN +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION, DOMAIN from homeassistant.const import CONF_PORT from tests.common import MockConfigEntry @@ -76,3 +76,22 @@ async def test_import_step(hass): assert result["type"] == "create_entry" assert result["title"] == test_data[CONF_PORT] assert result["data"] == test_data + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_PORT: "/dev/test"}) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_DEFAULT_TRANSITION: 12}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index c455d3a960e..1961843c8b0 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -2,7 +2,8 @@ import logging from homeassistant.components import light -from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_TRANSITION +from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from . import async_init_integration @@ -33,6 +34,55 @@ async def test_on_brightness(hass, mock_litejet): mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 0) +async def test_default_transition(hass, mock_litejet): + """Test turning the light on with the default transition option.""" + entry = await async_init_integration(hass) + + hass.config_entries.async_update_entry(entry, options={CONF_DEFAULT_TRANSITION: 12}) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" + + assert not light.is_on(hass, ENTITY_LIGHT) + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 102}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 12) + + +async def test_transition(hass, mock_litejet): + """Test turning the light on with transition.""" + await async_init_integration(hass) + + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" + + assert not light.is_on(hass, ENTITY_LIGHT) + + # On + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: 5}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 99, 5) + + # Off + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: 5}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 0, 5) + + async def test_on_off(hass, mock_litejet): """Test turning the light on and off.""" await async_init_integration(hass) @@ -91,9 +141,7 @@ async def test_activated_event(hass, mock_litejet): assert light.is_on(hass, ENTITY_OTHER_LIGHT) assert hass.states.get(ENTITY_LIGHT).state == "on" assert hass.states.get(ENTITY_OTHER_LIGHT).state == "on" - assert ( - int(hass.states.get(ENTITY_OTHER_LIGHT).attributes.get(ATTR_BRIGHTNESS)) == 103 - ) + assert hass.states.get(ENTITY_OTHER_LIGHT).attributes.get(ATTR_BRIGHTNESS) == 103 async def test_deactivated_event(hass, mock_litejet): From 16e8373fdd5a96723ca1d82d10abcfc0ac027611 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 24 Jul 2021 07:00:41 -0400 Subject: [PATCH 547/818] Use entity class attributes for advantage_air (#52498) * Use entity class attributes for advantage_air * update * tweak * tweak * use update listeners --- .../components/advantage_air/binary_sensor.py | 48 ++++---- .../components/advantage_air/climate.py | 112 ++++++------------ .../components/advantage_air/cover.py | 27 ++--- .../components/advantage_air/entity.py | 18 ++- .../components/advantage_air/sensor.py | 48 +++----- .../components/advantage_air/switch.py | 21 ++-- 6 files changed, 99 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index fba90148788..6a050e4086a 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -35,15 +35,13 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): _attr_device_class = DEVICE_CLASS_PROBLEM - @property - def name(self): - """Return the name.""" - return f'{self._ac["name"]} Filter' - - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-filter' + def __init__(self, instance, ac_key): + """Initialize an Advantage Air Filter.""" + super().__init__(instance, ac_key) + self._attr_name = f'{self._ac["name"]} Filter' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' + ) @property def is_on(self): @@ -56,15 +54,13 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): _attr_device_class = DEVICE_CLASS_MOTION - @property - def name(self): - """Return the name.""" - return f'{self._zone["name"]} Motion' - - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-motion' + def __init__(self, instance, ac_key, zone_key): + """Initialize an Advantage Air Zone Motion.""" + super().__init__(instance, ac_key, zone_key) + self._attr_name = f'{self._zone["name"]} Motion' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion' + ) @property def is_on(self): @@ -77,15 +73,13 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): _attr_entity_registry_enabled_default = False - @property - def name(self): - """Return the name.""" - return f'{self._zone["name"]} MyZone' - - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-myzone' + def __init__(self, instance, ac_key, zone_key): + """Initialize an Advantage Air Zone MyZone.""" + super().__init__(instance, ac_key, zone_key) + self._attr_name = f'{self._zone["name"]} MyZone' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone' + ) @property def is_on(self): diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d890fa43207..1d377abc065 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -1,5 +1,4 @@ """Climate platform for Advantage Air integration.""" - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, @@ -16,6 +15,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.helpers import entity_platform from .const import ( @@ -84,39 +84,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): """AdvantageAir Climate class.""" - @property - def temperature_unit(self): - """Return the temperature unit.""" - return TEMP_CELSIUS - - @property - def target_temperature_step(self): - """Return the supported temperature step.""" - return PRECISION_WHOLE - - @property - def max_temp(self): - """Return the maximum supported temperature.""" - return 32 - - @property - def min_temp(self): - """Return the minimum supported temperature.""" - return 16 + _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = PRECISION_WHOLE + _attr_max_temp = 32 + _attr_min_temp = 16 class AdvantageAirAC(AdvantageAirClimateEntity): """AdvantageAir AC unit.""" - @property - def name(self): - """Return the name.""" - return self._ac["name"] + _attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] + _attr_hvac_modes = AC_HVAC_MODES + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}' + def __init__(self, instance, ac_key): + """Initialize an AdvantageAir AC unit.""" + super().__init__(instance, ac_key) + self._attr_name = self._ac["name"] + self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}' + if self._ac.get("myAutoModeEnabled"): + self._attr_hvac_modes = AC_HVAC_MODES + [HVAC_MODE_AUTO] @property def target_temperature(self): @@ -130,28 +117,11 @@ class AdvantageAirAC(AdvantageAirClimateEntity): return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"]) return HVAC_MODE_OFF - @property - def hvac_modes(self): - """Return the supported HVAC modes.""" - if self._ac.get("myAutoModeEnabled"): - return AC_HVAC_MODES + [HVAC_MODE_AUTO] - return AC_HVAC_MODES - @property def fan_mode(self): """Return the current fan modes.""" return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) - @property - def fan_modes(self): - """Return the supported fan modes.""" - return [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] - - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - async def async_set_hvac_mode(self, hvac_mode): """Set the HVAC Mode and State.""" if hvac_mode == HVAC_MODE_OFF: @@ -185,42 +155,30 @@ class AdvantageAirAC(AdvantageAirClimateEntity): class AdvantageAirZone(AdvantageAirClimateEntity): """AdvantageAir Zone control.""" - @property - def name(self): - """Return the name.""" - return self._zone["name"] + _attr_hvac_modes = ZONE_HVAC_MODES + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' + def __init__(self, instance, ac_key, zone_key): + """Initialize an AdvantageAir Zone control.""" + super().__init__(instance, ac_key, zone_key) + self._attr_name = self._zone["name"] + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' + ) - @property - def current_temperature(self): - """Return the current temperature.""" - return self._zone["measuredTemp"] + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) - @property - def target_temperature(self): - """Return the target temperature.""" - return self._zone["setTemp"] - - @property - def hvac_mode(self): - """Return the current HVAC modes.""" + @callback + def _update_callback(self) -> None: + """Load data from integration.""" + self._attr_current_temperature = self._zone["measuredTemp"] + self._attr_target_temperature = self._zone["setTemp"] + self._attr_hvac_mode = HVAC_MODE_OFF if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: - return HVAC_MODE_FAN_ONLY - return HVAC_MODE_OFF - - @property - def hvac_modes(self): - """Return supported HVAC modes.""" - return ZONE_HVAC_MODES - - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_TARGET_TEMPERATURE + self._attr_hvac_mode = HVAC_MODE_FAN_ONLY + self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode): """Set the HVAC Mode and State.""" diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 69d66849cd6..04960dab002 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -36,25 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): """Advantage Air Cover Class.""" - @property - def name(self): - """Return the name.""" - return f'{self._zone["name"]}' + _attr_device_class = DEVICE_CLASS_DAMPER + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' - - @property - def device_class(self): - """Return the device class of the vent.""" - return DEVICE_CLASS_DAMPER - - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + def __init__(self, instance, ac_key, zone_key): + """Initialize an Advantage Air Cover Class.""" + super().__init__(instance, ac_key, zone_key) + self._attr_name = f'{self._zone["name"]}' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' + ) @property def is_closed(self): diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index ea20368c10f..ffb75e78c01 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -14,6 +14,13 @@ class AdvantageAirEntity(CoordinatorEntity): self.async_change = instance["async_change"] self.ac_key = ac_key self.zone_key = zone_key + self._attr_device_info = { + "identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])}, + "name": self.coordinator.data["system"]["name"], + "manufacturer": "Advantage Air", + "model": self.coordinator.data["system"]["sysType"], + "sw_version": self.coordinator.data["system"]["myAppRev"], + } @property def _ac(self): @@ -22,14 +29,3 @@ class AdvantageAirEntity(CoordinatorEntity): @property def _zone(self): return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] - - @property - def device_info(self): - """Return parent device information.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])}, - "name": self.coordinator.data["system"]["name"], - "manufacturer": "Advantage Air", - "model": self.coordinator.data["system"]["sysType"], - "sw_version": self.coordinator.data["system"]["myAppRev"], - } diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index e2bf90e73c3..eca7651d6eb 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -51,17 +51,11 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Initialize the Advantage Air timer control.""" super().__init__(instance, ac_key) self.action = action - self._time_key = f"countDownTo{self.action}" - - @property - def name(self): - """Return the name.""" - return f'{self._ac["name"]} Time To {self.action}' - - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{self.action}' + self._time_key = f"countDownTo{action}" + self._attr_name = f'{self._ac["name"]} Time To {action}' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' + ) @property def state(self): @@ -87,15 +81,13 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): _attr_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT - @property - def name(self): - """Return the name.""" - return f'{self._zone["name"]} Vent' - - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-vent' + def __init__(self, instance, ac_key, zone_key): + """Initialize an Advantage Air Zone Vent Sensor.""" + super().__init__(instance, ac_key, zone_key=zone_key) + self._attr_name = f'{self._zone["name"]} Vent' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent' + ) @property def state(self): @@ -118,15 +110,13 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): _attr_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT - @property - def name(self): - """Return the name.""" - return f'{self._zone["name"]} Signal' - - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-signal' + def __init__(self, instance, ac_key, zone_key): + """Initialize an Advantage Air Zone wireless signal sensor.""" + super().__init__(instance, ac_key, zone_key=zone_key) + self._attr_name = f'{self._zone["name"]} Signal' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' + ) @property def state(self): diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 6c687c1427e..1a44973f8c1 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -25,26 +25,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirFreshAir(AdvantageAirEntity, ToggleEntity): """Representation of Advantage Air fresh air control.""" - @property - def name(self): - """Return the name.""" - return f'{self._ac["name"]} Fresh Air' + _attr_icon = "mdi:air-filter" - @property - def unique_id(self): - """Return a unique id.""" - return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-freshair' + def __init__(self, instance, ac_key): + """Initialize an Advantage Air fresh air control.""" + super().__init__(instance, ac_key) + self._attr_name = f'{self._ac["name"]} Fresh Air' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair' + ) @property def is_on(self): """Return the fresh air status.""" return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON - @property - def icon(self): - """Return a representative icon of the fresh air switch.""" - return "mdi:air-filter" - async def async_turn_on(self, **kwargs): """Turn fresh air on.""" await self.async_change( From 745314b11596322b2ff1dd639852e6e75918e9ed Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 24 Jul 2021 14:03:04 +0200 Subject: [PATCH 548/818] Test KNX services (#53367) --- tests/components/knx/test_expose.py | 9 +- tests/components/knx/test_services.py | 166 ++++++++++++++++++++++++++ tests/components/knx/test_switch.py | 8 +- 3 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 tests/components/knx/test_services.py diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 6922b6ed926..25ec0f92604 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -2,9 +2,12 @@ from homeassistant.components.knx import CONF_KNX_EXPOSE, KNX_ADDRESS from homeassistant.components.knx.schema import ExposeSchema from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit -async def test_binary_expose(hass, knx): +async def test_binary_expose(hass: HomeAssistant, knx: KNXTestKit): """Test a binary expose to only send telegrams on state change.""" entity_id = "fake.entity" await knx.setup_integration( @@ -31,7 +34,7 @@ async def test_binary_expose(hass, knx): await knx.assert_write("1/1/8", False) -async def test_expose_attribute(hass, knx): +async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit): """Test an expose to only send telegrams on attribute change.""" entity_id = "fake.entity" attribute = "fake_attribute" @@ -76,7 +79,7 @@ async def test_expose_attribute(hass, knx): await knx.assert_telegram_count(0) -async def test_expose_attribute_with_default(hass, knx): +async def test_expose_attribute_with_default(hass: HomeAssistant, knx: KNXTestKit): """Test an expose to only send telegrams on attribute change.""" entity_id = "fake.entity" attribute = "fake_attribute" diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py new file mode 100644 index 00000000000..fe13a289a78 --- /dev/null +++ b/tests/components/knx/test_services.py @@ -0,0 +1,166 @@ +"""Test KNX services.""" +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + + +async def test_send(hass: HomeAssistant, knx: KNXTestKit): + """Test `knx.send` service.""" + test_address = "1/2/3" + await knx.setup_integration({}) + + # send DPT 1 telegram + await hass.services.async_call( + "knx", "send", {"address": test_address, "payload": True}, blocking=True + ) + await knx.assert_write(test_address, True) + + # send raw DPT 5 telegram + await hass.services.async_call( + "knx", "send", {"address": test_address, "payload": [99]}, blocking=True + ) + await knx.assert_write(test_address, (99,)) + + # send "percent" DPT 5 telegram + await hass.services.async_call( + "knx", + "send", + {"address": test_address, "payload": 99, "type": "percent"}, + blocking=True, + ) + await knx.assert_write(test_address, (0xFC,)) + + # send "temperature" DPT 9 telegram + await hass.services.async_call( + "knx", + "send", + {"address": test_address, "payload": 21.0, "type": "temperature"}, + blocking=True, + ) + await knx.assert_write(test_address, (0x0C, 0x1A)) + + # send multiple telegrams + await hass.services.async_call( + "knx", + "send", + {"address": [test_address, "2/2/2", "3/3/3"], "payload": 99, "type": "percent"}, + blocking=True, + ) + await knx.assert_write(test_address, (0xFC,)) + await knx.assert_write("2/2/2", (0xFC,)) + await knx.assert_write("3/3/3", (0xFC,)) + + +async def test_read(hass: HomeAssistant, knx: KNXTestKit): + """Test `knx.read` service.""" + await knx.setup_integration({}) + + # send read telegram + await hass.services.async_call("knx", "read", {"address": "1/1/1"}, blocking=True) + await knx.assert_read("1/1/1") + + # send multiple read telegrams + await hass.services.async_call( + "knx", + "read", + {"address": ["1/1/1", "2/2/2", "3/3/3"]}, + blocking=True, + ) + await knx.assert_read("1/1/1") + await knx.assert_read("2/2/2") + await knx.assert_read("3/3/3") + + +async def test_event_register(hass: HomeAssistant, knx: KNXTestKit): + """Test `knx.event_register` service.""" + events = [] + test_address = "1/2/3" + + def listener(event): + events.append(event) + + await knx.setup_integration({}) + hass.bus.async_listen("knx_event", listener) + + # no event registered + await knx.receive_write(test_address, True) + assert len(events) == 0 + + # register event + await hass.services.async_call( + "knx", "event_register", {"address": test_address}, blocking=True + ) + await knx.receive_write(test_address, True) + await knx.receive_write(test_address, False) + assert len(events) == 2 + + # remove event registration + events = [] + await hass.services.async_call( + "knx", + "event_register", + {"address": test_address, "remove": True}, + blocking=True, + ) + await knx.receive_write(test_address, True) + assert len(events) == 0 + + +async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit): + """Test `knx.exposure_register` service.""" + test_address = "1/2/3" + test_entity = "fake.entity" + test_attribute = "fake_attribute" + + await knx.setup_integration({}) + + # no exposure registered + hass.states.async_set(test_entity, STATE_ON, {}) + await knx.assert_no_telegram() + + # register exposure + await hass.services.async_call( + "knx", + "exposure_register", + {"address": test_address, "entity_id": test_entity, "type": "binary"}, + blocking=True, + ) + hass.states.async_set(test_entity, STATE_OFF, {}) + await knx.assert_write(test_address, False) + + # register exposure + await hass.services.async_call( + "knx", + "exposure_register", + {"address": test_address, "remove": True}, + blocking=True, + ) + hass.states.async_set(test_entity, STATE_ON, {}) + await knx.assert_no_telegram() + + # register exposure for attribute with default + await hass.services.async_call( + "knx", + "exposure_register", + { + "address": test_address, + "entity_id": test_entity, + "attribute": test_attribute, + "type": "percentU8", + "default": 0, + }, + blocking=True, + ) + # no attribute on first change wouldn't work because no attribute change since last test + hass.states.async_set(test_entity, STATE_ON, {test_attribute: 30}) + await knx.assert_write(test_address, (30,)) + hass.states.async_set(test_entity, STATE_OFF, {}) + await knx.assert_write(test_address, (0,)) + # don't send same value sequentially + hass.states.async_set(test_entity, STATE_ON, {test_attribute: 25}) + hass.states.async_set(test_entity, STATE_ON, {test_attribute: 25}) + hass.states.async_set(test_entity, STATE_ON, {test_attribute: 25, "unrelated": 2}) + hass.states.async_set(test_entity, STATE_OFF, {test_attribute: 25}) + await knx.assert_telegram_count(1) + await knx.assert_write(test_address, (25,)) diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index 407d6d83267..eff34243ca8 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -8,10 +8,12 @@ from homeassistant.components.knx.const import ( ) from homeassistant.components.knx.schema import SwitchSchema from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON -from homeassistant.core import State +from homeassistant.core import HomeAssistant, State + +from .conftest import KNXTestKit -async def test_switch_simple(hass, knx): +async def test_switch_simple(hass: HomeAssistant, knx: KNXTestKit): """Test simple KNX switch.""" await knx.setup_integration( { @@ -50,7 +52,7 @@ async def test_switch_simple(hass, knx): await knx.assert_telegram_count(0) -async def test_switch_state(hass, knx): +async def test_switch_state(hass: HomeAssistant, knx: KNXTestKit): """Test KNX switch with state_address.""" _ADDRESS = "1/1/1" _STATE_ADDRESS = "2/2/2" From 6d493a848c252860d35d8aaf6beacc81c1897dc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Jul 2021 02:07:10 -1000 Subject: [PATCH 549/818] Ensure PyPI packages can still be installed on high latency connections (#53365) --- homeassistant/requirements.py | 2 ++ homeassistant/util/package.py | 3 +++ tests/test_requirements.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 718ceba54ab..67d0ede96bc 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -14,6 +14,7 @@ import homeassistant.util.package as pkg_util # mypy: disallow-any-generics +PIP_TIMEOUT = 60 # The default is too low when the internet connection is satellite or high latency DATA_PIP_LOCK = "pip_lock" DATA_PKG_CACHE = "pkg_cache" DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs" @@ -169,6 +170,7 @@ def pip_kwargs(config_dir: str | None) -> dict[str, Any]: kwargs = { "constraints": os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE), "no_cache_dir": is_docker, + "timeout": PIP_TIMEOUT, } if "WHEELS_LINKS" in os.environ: kwargs["find_links"] = os.environ["WHEELS_LINKS"] diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 50d46b6c469..609d09e4f55 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -63,6 +63,7 @@ def install_package( target: str | None = None, constraints: str | None = None, find_links: str | None = None, + timeout: int | None = None, no_cache_dir: bool | None = False, ) -> bool: """Install a package on PyPi. Accepts pip compatible package strings. @@ -73,6 +74,8 @@ def install_package( _LOGGER.info("Attempting install of %s", package) env = os.environ.copy() args = [sys.executable, "-m", "pip", "install", "--quiet", package] + if timeout: + args += ["--timeout", str(timeout)] if no_cache_dir: args.append("--no-cache-dir") if upgrade: diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 26f3603910d..82ce10872bf 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -38,6 +38,7 @@ async def test_requirement_installed_in_venv(hass): assert mock_install.call_args == call( "package==0.0.1", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), + timeout=60, no_cache_dir=False, ) @@ -59,6 +60,7 @@ async def test_requirement_installed_in_deps(hass): "package==0.0.1", target=hass.config.path("deps"), constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), + timeout=60, no_cache_dir=False, ) @@ -304,6 +306,7 @@ async def test_install_with_wheels_index(hass): "hello==1.0.0", find_links="https://wheels.hass.io/test", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), + timeout=60, no_cache_dir=True, ) @@ -327,6 +330,7 @@ async def test_install_on_docker(hass): assert mock_inst.call_args == call( "hello==1.0.0", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), + timeout=60, no_cache_dir=True, ) From e65283389d8e0bf66b3c2760f1c25d835bcbf5ed Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 24 Jul 2021 14:28:33 +0200 Subject: [PATCH 550/818] Update Plugwise config_flow once more (#53423) --- homeassistant/components/plugwise/config_flow.py | 11 +---------- homeassistant/components/plugwise/const.py | 4 ++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index abecda7f728..5f32aca13a0 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -42,14 +42,7 @@ _LOGGER = logging.getLogger(__name__) CONF_MANUAL_PATH = "Enter Manually" CONNECTION_SCHEMA = vol.Schema( - { - vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In( - { - FLOW_NET: f"Network: {SMILE} / {STRETCH}", - FLOW_USB: "USB: To be added later", - } - ), - }, + {vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])} ) # PLACEHOLDER USB connection validation @@ -148,8 +141,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_PORT] = self.discovery_info[CONF_PORT] user_input[CONF_USERNAME] = self.discovery_info[CONF_USERNAME] - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - try: api = await validate_gw_input(self.hass, user_input) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index a7afbd9e197..a7a5c92a21c 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -5,11 +5,11 @@ ATTR_ILLUMINANCE = "illuminance" COORDINATOR = "coordinator" DEVICE_STATE = "device_state" DOMAIN = "plugwise" -FLOW_NET = "flow_network" +FLOW_NET = "Network: Smile/Stretch" FLOW_SMILE = "smile (Adam/Anna/P1)" FLOW_STRETCH = "stretch (Stretch)" FLOW_TYPE = "flow_type" -FLOW_USB = "flow_usb" +FLOW_USB = "USB: Stick - Coming soon" GATEWAY = "gateway" PW_TYPE = "plugwise_type" SCHEDULE_OFF = "false" From 5c86cc502f596782d6a57e7ef05646adbd3514ff Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 24 Jul 2021 14:44:50 +0200 Subject: [PATCH 551/818] Bump to py-synologydsm-api 1.0.3 (#53402) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index afa8e2674de..04d7f43bb75 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["synologydsm-api==1.0.2"], + "requirements": ["py-synologydsm-api==1.0.3"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4b07a6e6d70..752b664cb5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1254,6 +1254,9 @@ py-nightscout==1.2.2 # homeassistant.components.schluter py-schluter==0.1.7 +# homeassistant.components.synology_dsm +py-synologydsm-api==1.0.3 + # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -2225,9 +2228,6 @@ swisshydrodata==0.1.0 # homeassistant.components.synology_srm synology-srm==0.2.0 -# homeassistant.components.synology_dsm -synologydsm-api==1.0.2 - # homeassistant.components.system_bridge systembridge==1.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51ca22f2b03..84b9e458b92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,6 +706,9 @@ py-melissa-climate==2.1.4 # homeassistant.components.nightscout py-nightscout==1.2.2 +# homeassistant.components.synology_dsm +py-synologydsm-api==1.0.3 + # homeassistant.components.seventeentrack py17track==3.2.1 @@ -1227,9 +1230,6 @@ sunwatcher==0.2.1 # homeassistant.components.surepetcare surepy==0.7.0 -# homeassistant.components.synology_dsm -synologydsm-api==1.0.2 - # homeassistant.components.system_bridge systembridge==1.1.5 From 54ace4cdd437b4b79ea2e4fd8b3ed5fb8311e9fe Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 24 Jul 2021 06:50:01 -0600 Subject: [PATCH 552/818] Enforce strict typing for OpenUV (#53409) * Enforce strict typing for OpenUV * Linting * Fix tests --- .strict-typing | 1 + homeassistant/components/openuv/__init__.py | 49 ++++++++++--------- .../components/openuv/binary_sensor.py | 40 +++++++++------ .../components/openuv/config_flow.py | 15 ++++-- homeassistant/components/openuv/sensor.py | 26 +++++++--- mypy.ini | 11 +++++ tests/components/openuv/test_config_flow.py | 16 +----- 7 files changed, 92 insertions(+), 66 deletions(-) diff --git a/.strict-typing b/.strict-typing index 0a226f973f6..240be148a03 100644 --- a/.strict-typing +++ b/.strict-typing @@ -68,6 +68,7 @@ homeassistant.components.notify.* homeassistant.components.notion.* homeassistant.components.number.* homeassistant.components.onewire.* +homeassistant.components.openuv.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.proximity.* diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index df63dd91b2e..0de97e52cbe 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,9 +1,14 @@ """Support for UV data from openuv.io.""" +from __future__ import annotations + import asyncio +from collections.abc import MutableMapping +from typing import Any from pyopenuv import Client from pyopenuv.errors import OpenUvError +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -13,7 +18,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SENSORS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import ( @@ -42,14 +47,10 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update" PLATFORMS = ["binary_sensor", "sensor"] -async def async_setup(hass, config): - """Set up the OpenUV component.""" - hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} - return True - - -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}}) + _verify_domain_control = verify_domain_control(hass, DOMAIN) try: @@ -72,21 +73,21 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @_verify_domain_control - async def update_data(service): + async def update_data(_: ServiceCall) -> None: """Refresh all OpenUV data.""" LOGGER.debug("Refreshing all OpenUV data") await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control - async def update_uv_index_data(service): + async def update_uv_index_data(_: ServiceCall) -> None: """Refresh OpenUV UV index data.""" LOGGER.debug("Refreshing OpenUV UV index data") await openuv.async_update_uv_index_data() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control - async def update_protection_data(service): + async def update_protection_data(_: ServiceCall) -> None: """Refresh OpenUV protection window data.""" LOGGER.debug("Refreshing OpenUV protection window data") await openuv.async_update_protection_data() @@ -102,7 +103,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload an OpenUV config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS @@ -113,7 +114,7 @@ async def async_unload_entry(hass, config_entry): return unload_ok -async def async_migrate_entry(hass, config_entry): +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate the config entry upon new versions.""" version = config_entry.version data = {**config_entry.data} @@ -134,12 +135,12 @@ async def async_migrate_entry(hass, config_entry): class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, client): + def __init__(self, client: Client) -> None: """Initialize.""" self.client = client - self.data = {} + self.data: dict[str, Any] = {} - async def async_update_protection_data(self): + async def async_update_protection_data(self) -> None: """Update binary sensor (protection window) data.""" try: resp = await self.client.uv_protection_window() @@ -148,7 +149,7 @@ class OpenUV: LOGGER.error("Error during protection data update: %s", err) self.data[DATA_PROTECTION_WINDOW] = {} - async def async_update_uv_index_data(self): + async def async_update_uv_index_data(self) -> None: """Update sensor (uv index, etc) data.""" try: data = await self.client.uv_index() @@ -157,7 +158,7 @@ class OpenUV: LOGGER.error("Error during uv index data update: %s", err) self.data[DATA_UV] = {} - async def async_update(self): + async def async_update(self) -> None: """Update sensor/binary sensor data.""" tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()] await asyncio.gather(*tasks) @@ -166,9 +167,11 @@ class OpenUV: class OpenUvEntity(Entity): """Define a generic OpenUV entity.""" - def __init__(self, openuv, sensor_type): + def __init__(self, openuv: OpenUV, sensor_type: str) -> None: """Initialize.""" - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes: MutableMapping[str, Any] = { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION + } self._attr_should_poll = False self._attr_unique_id = ( f"{openuv.client.latitude}_{openuv.client.longitude}_{sensor_type}" @@ -176,11 +179,11 @@ class OpenUvEntity(Entity): self._sensor_type = sensor_type self.openuv = openuv - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" self.update_from_latest_data() self.async_write_ha_state() @@ -189,6 +192,6 @@ class OpenUvEntity(Entity): self.update_from_latest_data() - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the sensor using the latest data.""" raise NotImplementedError diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index eac67909b86..12b1f0c82af 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -1,9 +1,11 @@ """Support for OpenUV binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_local, parse_datetime, utcnow -from . import OpenUvEntity +from . import OpenUV, OpenUvEntity from .const import ( DATA_CLIENT, DATA_PROTECTION_WINDOW, @@ -20,7 +22,9 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses")} -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up an OpenUV sensor based on a config entry.""" openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] @@ -35,7 +39,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon): + def __init__(self, openuv: OpenUV, sensor_type: str, name: str, icon: str) -> None: """Initialize the sensor.""" super().__init__(openuv, sensor_type) @@ -43,7 +47,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): self._attr_name = name @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the state.""" data = self.openuv.data[DATA_PROTECTION_WINDOW] @@ -59,20 +63,24 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): return if self._sensor_type == TYPE_PROTECTION_WINDOW: - self._attr_is_on = ( - parse_datetime(data["from_time"]) - <= utcnow() - <= parse_datetime(data["to_time"]) - ) + from_dt = parse_datetime(data["from_time"]) + to_dt = parse_datetime(data["to_time"]) + + if not from_dt or not to_dt: + LOGGER.warning( + "Unable to parse protection window datetimes: %s, %s", + data["from_time"], + data["to_time"], + ) + self._attr_is_on = False + return + + self._attr_is_on = from_dt <= utcnow() <= to_dt self._attr_extra_state_attributes.update( { - ATTR_PROTECTION_WINDOW_ENDING_TIME: as_local( - parse_datetime(data["to_time"]) - ), + ATTR_PROTECTION_WINDOW_ENDING_TIME: as_local(to_dt), ATTR_PROTECTION_WINDOW_ENDING_UV: data["to_uv"], ATTR_PROTECTION_WINDOW_STARTING_UV: data["from_uv"], - ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local( - parse_datetime(data["from_time"]) - ), + ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt), } ) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index e31cef9ee0a..54b2aca0b75 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,4 +1,8 @@ """Config flow to configure the OpenUV component.""" +from __future__ import annotations + +from typing import Any + from pyopenuv import Client from pyopenuv.errors import OpenUvError import voluptuous as vol @@ -10,6 +14,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN @@ -21,7 +26,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 @property - def config_schema(self): + def config_schema(self) -> vol.Schema: """Return the config schema.""" return vol.Schema( { @@ -38,7 +43,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _show_form(self, errors=None): + async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -46,11 +51,13 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - async def async_step_import(self, import_config): + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return await self._show_form() diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 6f4e4e18d34..386527ebc3e 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -1,10 +1,14 @@ """Support for OpenUV sensors.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MINUTES, UV_INDEX -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_local, parse_datetime -from . import OpenUvEntity +from . import OpenUV, OpenUvEntity from .const import ( DATA_CLIENT, DATA_UV, @@ -76,7 +80,9 @@ SENSORS = { } -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up a OpenUV sensor based on a config entry.""" openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] @@ -91,7 +97,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class OpenUvSensor(OpenUvEntity, SensorEntity): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon, unit): + def __init__( + self, openuv: OpenUV, sensor_type: str, name: str, icon: str, unit: str | None + ) -> None: """Initialize the sensor.""" super().__init__(openuv, sensor_type) @@ -100,7 +108,7 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_unit_of_measurement = unit @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the state.""" data = self.openuv.data[DATA_UV].get("result") @@ -127,9 +135,11 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_state = UV_LEVEL_LOW elif self._sensor_type == TYPE_MAX_UV_INDEX: self._attr_state = data["uv_max"] - self._attr_extra_state_attributes.update( - {ATTR_MAX_UV_TIME: as_local(parse_datetime(data["uv_max_time"]))} - ) + uv_max_time = parse_datetime(data["uv_max_time"]) + if uv_max_time: + self._attr_extra_state_attributes.update( + {ATTR_MAX_UV_TIME: as_local(uv_max_time)} + ) elif self._sensor_type in ( TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2, diff --git a/mypy.ini b/mypy.ini index 014272f0022..5c3b2835f72 100644 --- a/mypy.ini +++ b/mypy.ini @@ -759,6 +759,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.openuv.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.persistent_notification.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 83626c2d9f6..3feeb2638b4 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch from pyopenuv.errors import InvalidApiKeyError -import pytest from homeassistant import data_entry_flow from homeassistant.components.openuv import DOMAIN @@ -17,19 +16,6 @@ from homeassistant.const import ( from tests.common import MockConfigEntry -@pytest.fixture(autouse=True) -def mock_setup(): - """Prevent setup.""" - with patch( - "homeassistant.components.openuv.async_setup", - return_value=True, - ), patch( - "homeassistant.components.openuv.async_setup_entry", - return_value=True, - ): - yield - - async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" conf = { @@ -81,7 +67,7 @@ async def test_step_user(hass): } with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True + "homeassistant.components.openuv.async_setup_entry", return_value=True ), patch("pyopenuv.client.Client.uv_index"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} From 993756e90abb949311eabffd778e46db6a2144ad Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 24 Jul 2021 15:25:58 +0200 Subject: [PATCH 553/818] Test KNX select (#53371) * test KNX select * cover everything * Update tests/components/knx/test_select.py --- tests/components/knx/test_select.py | 187 ++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 tests/components/knx/test_select.py diff --git a/tests/components/knx/test_select.py b/tests/components/knx/test_select.py new file mode 100644 index 00000000000..d8089976aca --- /dev/null +++ b/tests/components/knx/test_select.py @@ -0,0 +1,187 @@ +"""Test KNX select.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.knx.const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, + KNX_ADDRESS, +) +from homeassistant.components.knx.schema import SelectSchema +from homeassistant.const import CONF_NAME, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, State + +from .conftest import KNXTestKit + + +async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit): + """Test simple KNX select.""" + _options = [ + {SelectSchema.CONF_PAYLOAD: 0b00, SelectSchema.CONF_OPTION: "No control"}, + {SelectSchema.CONF_PAYLOAD: 0b10, SelectSchema.CONF_OPTION: "Control - Off"}, + {SelectSchema.CONF_PAYLOAD: 0b11, SelectSchema.CONF_OPTION: "Control - On"}, + ] + test_address = "1/1/1" + await knx.setup_integration( + { + SelectSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_SYNC_STATE: False, + SelectSchema.CONF_PAYLOAD_LENGTH: 0, + SelectSchema.CONF_OPTIONS: _options, + } + } + ) + assert len(hass.states.async_all()) == 1 + state = hass.states.get("select.test") + assert state.state is STATE_UNKNOWN + + # select an option + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.test", "option": "Control - Off"}, + blocking=True, + ) + await knx.assert_write(test_address, 0b10) + state = hass.states.get("select.test") + assert state.state == "Control - Off" + + # select another option + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.test", "option": "No control"}, + blocking=True, + ) + await knx.assert_write(test_address, 0b00) + state = hass.states.get("select.test") + assert state.state == "No control" + + # don't answer to GroupValueRead requests by default + await knx.receive_read(test_address) + await knx.assert_no_telegram() + + # update from KNX + await knx.receive_write(test_address, 0b11) + state = hass.states.get("select.test") + assert state.state == "Control - On" + + # update from KNX with undefined value + await knx.receive_write(test_address, 0b01) + state = hass.states.get("select.test") + assert state.state is STATE_UNKNOWN + + # select invalid option + with pytest.raises(ValueError): + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.test", "option": "invalid"}, + blocking=True, + ) + await knx.assert_no_telegram() + + +async def test_select_dpt_2_restore(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX select with passive_address and respond_to_read restoring state.""" + _options = [ + {SelectSchema.CONF_PAYLOAD: 0b00, SelectSchema.CONF_OPTION: "No control"}, + {SelectSchema.CONF_PAYLOAD: 0b10, SelectSchema.CONF_OPTION: "Control - Off"}, + {SelectSchema.CONF_PAYLOAD: 0b11, SelectSchema.CONF_OPTION: "Control - On"}, + ] + test_address = "1/1/1" + test_passive_address = "3/3/3" + fake_state = State("select.test", "Control - On") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + await knx.setup_integration( + { + SelectSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_RESPOND_TO_READ: True, + SelectSchema.CONF_PAYLOAD_LENGTH: 0, + SelectSchema.CONF_OPTIONS: _options, + } + } + ) + # restored state - doesn't send telegram + state = hass.states.get("select.test") + assert state.state == "Control - On" + await knx.assert_telegram_count(0) + + # respond with restored state + await knx.receive_read(test_address) + await knx.assert_response(test_address, 3) + + # don't respond to passive address + await knx.receive_read(test_passive_address) + await knx.assert_no_telegram() + + +async def test_select_dpt_20_103_all_options(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX select with state_address, passive_address and respond_to_read.""" + _options = [ + {SelectSchema.CONF_PAYLOAD: 0, SelectSchema.CONF_OPTION: "Auto"}, + {SelectSchema.CONF_PAYLOAD: 1, SelectSchema.CONF_OPTION: "Legio protect"}, + {SelectSchema.CONF_PAYLOAD: 2, SelectSchema.CONF_OPTION: "Normal"}, + {SelectSchema.CONF_PAYLOAD: 3, SelectSchema.CONF_OPTION: "Reduced"}, + {SelectSchema.CONF_PAYLOAD: 4, SelectSchema.CONF_OPTION: "Off"}, + ] + test_address = "1/1/1" + test_state_address = "2/2/2" + test_passive_address = "3/3/3" + + await knx.setup_integration( + { + SelectSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_STATE_ADDRESS: test_state_address, + CONF_RESPOND_TO_READ: True, + SelectSchema.CONF_PAYLOAD_LENGTH: 1, + SelectSchema.CONF_OPTIONS: _options, + } + } + ) + assert len(hass.states.async_all()) == 1 + state = hass.states.get("select.test") + assert state.state is STATE_UNKNOWN + + # StateUpdater initialize state + await knx.assert_read(test_state_address) + await knx.receive_response(test_state_address, (2,)) + state = hass.states.get("select.test") + assert state.state == "Normal" + + # select an option + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.test", "option": "Legio protect"}, + blocking=True, + ) + await knx.assert_write(test_address, (1,)) + state = hass.states.get("select.test") + assert state.state == "Legio protect" + + # answer to GroupValueRead requests + await knx.receive_read(test_address) + await knx.assert_response(test_address, (1,)) + + # update from KNX state_address + await knx.receive_write(test_state_address, (3,)) + state = hass.states.get("select.test") + assert state.state == "Reduced" + + # update from KNX passive_address + await knx.receive_write(test_passive_address, (4,)) + state = hass.states.get("select.test") + assert state.state == "Off" From ea1ec91c9cca09127911392e8b38cbc9a35ba640 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Sat, 24 Jul 2021 23:49:39 +1000 Subject: [PATCH 554/818] Upgrade open-garage to 0.1.5 (#53412) --- homeassistant/components/opengarage/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index a14fb232eac..b6c617408b5 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -3,6 +3,6 @@ "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", "codeowners": ["@danielhiversen"], - "requirements": ["open-garage==0.1.4"], + "requirements": ["open-garage==0.1.5"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 752b664cb5e..a25df049f64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1088,7 +1088,7 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==1.0.0 # homeassistant.components.opengarage -open-garage==0.1.4 +open-garage==0.1.5 # homeassistant.components.opencv # opencv-python-headless==4.5.2.54 From 87e41e807c284703977c4cc29a207a5aa0d86ffe Mon Sep 17 00:00:00 2001 From: Bram Gerritsen Date: Sat, 24 Jul 2021 15:52:14 +0200 Subject: [PATCH 555/818] Add support for Velux light devices (#49338) Co-authored-by: Franck Nijhof --- homeassistant/components/velux/__init__.py | 43 ++++++++++++++++- homeassistant/components/velux/cover.py | 38 +-------------- homeassistant/components/velux/light.py | 56 ++++++++++++++++++++++ 3 files changed, 100 insertions(+), 37 deletions(-) create mode 100644 homeassistant/components/velux/light.py diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 5c1d8bfd370..6783c71b3cd 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -5,12 +5,14 @@ from pyvlx import PyVLX, PyVLXException import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity DOMAIN = "velux" DATA_VELUX = "data_velux" -PLATFORMS = ["cover", "scene"] +PLATFORMS = ["cover", "light", "scene"] _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( @@ -75,3 +77,42 @@ class VeluxModule: _LOGGER.debug("Velux interface started") await self.pyvlx.load_scenes() await self.pyvlx.load_nodes() + + +class VeluxEntity(Entity): + """Abstraction for al Velux entities.""" + + def __init__(self, node): + """Initialize the Velux device.""" + self.node = node + + @callback + def async_register_callbacks(self): + """Register callbacks to update hass after device was changed.""" + + async def after_update_callback(device): + """Call after device was updated.""" + self.async_write_ha_state() + + self.node.register_device_updated_cb(after_update_callback) + + async def async_added_to_hass(self): + """Store register state change callback.""" + self.async_register_callbacks() + + @property + def unique_id(self) -> str: + """Return the unique id base on the serial_id returned by Velux.""" + return self.node.serial_number + + @property + def name(self): + """Return the name of the Velux device.""" + if not self.node.name: + return "#" + str(self.node.node_id) + return self.node.name + + @property + def should_poll(self): + """No polling needed within Velux.""" + return False diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 187c0d36178..3bb228dd425 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -21,9 +21,8 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.core import callback -from . import DATA_VELUX +from . import DATA_VELUX, VeluxEntity async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -35,42 +34,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class VeluxCover(CoverEntity): +class VeluxCover(VeluxEntity, CoverEntity): """Representation of a Velux cover.""" - def __init__(self, node): - """Initialize the cover.""" - self.node = node - - @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - - async def after_update_callback(device): - """Call after device was updated.""" - self.async_write_ha_state() - - self.node.register_device_updated_cb(after_update_callback) - - async def async_added_to_hass(self): - """Store register state change callback.""" - self.async_register_callbacks() - - @property - def unique_id(self): - """Return the unique ID of this cover.""" - return self.node.serial_number - - @property - def name(self): - """Return the name of the Velux device.""" - return self.node.name - - @property - def should_poll(self): - """No polling needed within Velux.""" - return False - @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py new file mode 100644 index 00000000000..2e33b267e43 --- /dev/null +++ b/homeassistant/components/velux/light.py @@ -0,0 +1,56 @@ +"""Support for Velux lights.""" +from pyvlx import Intensity, LighteningDevice +from pyvlx.node import Node + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, + LightEntity, +) + +from . import DATA_VELUX, VeluxEntity + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up light(s) for Velux platform.""" + async_add_entities( + VeluxLight(node) + for node in hass.data[DATA_VELUX].pyvlx.nodes + if isinstance(node, LighteningDevice) + ) + + +class VeluxLight(VeluxEntity, LightEntity): + """Representation of a Velux light.""" + + def __init__(self, node: Node) -> None: + """Initialize the Velux light.""" + super().__init__(node) + + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + + @property + def brightness(self): + """Return the current brightness.""" + return int((100 - self.node.intensity.intensity_percent) * 255 / 100) + + @property + def is_on(self): + """Return true if light is on.""" + return not self.node.intensity.off and self.node.intensity.known + + async def async_turn_on(self, **kwargs): + """Instruct the light to turn on.""" + if ATTR_BRIGHTNESS in kwargs: + intensity_percent = int(100 - kwargs[ATTR_BRIGHTNESS] / 255 * 100) + await self.node.set_intensity( + Intensity(intensity_percent=intensity_percent), + wait_for_completion=True, + ) + else: + await self.node.turn_on(wait_for_completion=True) + + async def async_turn_off(self, **kwargs): + """Instruct the light to turn off.""" + await self.node.turn_off(wait_for_completion=True) From 0f15d2bf19605cb56aee29b7ee7eceda106d7ea8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Jul 2021 04:31:30 -1000 Subject: [PATCH 556/818] Ensure HomeKit accessories are started again after reset (#53372) --- homeassistant/components/homekit/__init__.py | 7 ++++++- tests/components/homekit/test_homekit.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index a1203b25478..f85c8ad5063 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -557,6 +557,7 @@ class HomeKit: return if new_acc := self._async_create_single_accessory([state]): self.driver.accessory = new_acc + self.hass.async_add_job(new_acc.run) await self.async_config_changed() async def async_reset_accessories_in_bridge_mode(self, entity_ids): @@ -586,7 +587,9 @@ class HomeKit: await self.async_config_changed() await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME) for state in new: - self.add_bridge_accessory(state) + acc = self.add_bridge_accessory(state) + if acc: + self.hass.async_add_job(acc.run) await self.async_config_changed() async def async_config_changed(self): @@ -625,10 +628,12 @@ class HomeKit: acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: self.bridge.add_accessory(acc) + return acc except Exception: # pylint: disable=broad-except _LOGGER.exception( "Failed to create a HomeKit accessory for %s", state.entity_id ) + return None def remove_bridge_accessory(self, aid): """Try adding accessory to bridge if configured beforehand.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 6539a7137d3..138c8fd8209 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -646,7 +646,9 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): "pyhap.accessory_driver.AccessoryDriver.config_changed" ) as hk_driver_config_changed, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object( + ), patch( + f"{PATH_HOMEKIT}.accessories.HomeAccessory.run" + ) as mock_run, patch.object( homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 ): await async_init_entry(hass, entry) @@ -667,6 +669,7 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): assert hk_driver_config_changed.call_count == 2 assert mock_add_accessory.called + assert mock_run.called homekit.status = STATUS_READY @@ -923,7 +926,9 @@ async def test_homekit_reset_single_accessory(hass, mock_zeroconf): "pyhap.accessory_driver.AccessoryDriver.config_changed" ) as hk_driver_config_changed, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" - ): + ), patch( + f"{PATH_HOMEKIT}.accessories.HomeAccessory.run" + ) as mock_run: await async_init_entry(hass, entry) homekit.status = STATUS_RUNNING @@ -938,7 +943,7 @@ async def test_homekit_reset_single_accessory(hass, mock_zeroconf): blocking=True, ) await hass.async_block_till_done() - + assert mock_run.called assert hk_driver_config_changed.call_count == 1 homekit.status = STATUS_READY From f0d5ae2fec475cc28d102efab2834965bc8ecb44 Mon Sep 17 00:00:00 2001 From: G Johansson <62932417+gjohansson-ST@users.noreply.github.com> Date: Sat, 24 Jul 2021 19:55:43 +0200 Subject: [PATCH 557/818] Add yale_smart_alarm config flow and coordinator (#50850) * config flow and coordinator * comply with pylint * Remove pylint errors * Update test coverage yale smart alarm * Update test config_flow * Fix test already configured * Second try test already configured * Fixes config flow and tests * Conform pylint errors coordinator * Fix various review remarks * Correct entity unique id * Fix unique id and migrate entries * Remove lock code * Remove code from test * Expand code coverage config flow test * Add more constants * Add test new requirements * Minor corrections * Resolve conflict alarm schema * Change logger * Changed from review * Fix isort error * Fix flake error * Ignore mypy errors * Corrections from PR review no 2 * Corrections from PR review no 3 * Added tests and fix pylint error * Corrections from PR review no 4 * Corrections from PR review no 5 * Corrections from PR review no 6 * Corrections from PR review no 6_2 * Corrections from PR review no 7 * Corrections from PR review no 8 * Minor last changes for PR * Update homeassistant/components/yale_smart_alarm/coordinator.py Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> --- .coveragerc | 3 + .../components/yale_smart_alarm/__init__.py | 45 ++++ .../yale_smart_alarm/alarm_control_panel.py | 138 ++++++----- .../yale_smart_alarm/config_flow.py | 129 ++++++++++ .../components/yale_smart_alarm/const.py | 39 +++ .../yale_smart_alarm/coordinator.py | 139 +++++++++++ .../components/yale_smart_alarm/manifest.json | 1 + .../components/yale_smart_alarm/strings.json | 28 +++ .../yale_smart_alarm/translations/en.json | 28 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/yale_smart_alarm/__init__.py | 1 + .../yale_smart_alarm/test_config_flow.py | 224 ++++++++++++++++++ 13 files changed, 722 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/yale_smart_alarm/config_flow.py create mode 100644 homeassistant/components/yale_smart_alarm/const.py create mode 100644 homeassistant/components/yale_smart_alarm/coordinator.py create mode 100644 homeassistant/components/yale_smart_alarm/strings.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/en.json create mode 100644 tests/components/yale_smart_alarm/__init__.py create mode 100644 tests/components/yale_smart_alarm/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 4be3d2e5f01..759e2251988 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1214,7 +1214,10 @@ omit = homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* + homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py + homeassistant/components/yale_smart_alarm/const.py + homeassistant/components/yale_smart_alarm/coordinator.py homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yandex_transport/* diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 2ce2fb13495..87bfb2b86d7 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -1 +1,46 @@ """The yale_smart_alarm component.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed + +from .const import DOMAIN, LOGGER, PLATFORMS +from .coordinator import YaleDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yale from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + title = entry.title + + coordinator = YaleDataUpdateCoordinator(hass, entry=entry) + + if not await hass.async_add_executor_job(coordinator.get_updates): + raise ConfigEntryAuthFailed + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + "coordinator": coordinator, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + LOGGER.debug("Loaded entry for %s", title) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + title = entry.title + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + LOGGER.debug("Unloaded entry for %s", title) + return unload_ok + + return False diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 13433086879..f450895f5c3 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -1,14 +1,7 @@ -"""Component for interacting with the Yale Smart Alarm System API.""" -import logging +"""Support for Yale Alarm.""" +from __future__ import annotations import voluptuous as vol -from yalesmartalarmclient.client import ( - YALE_STATE_ARM_FULL, - YALE_STATE_ARM_PARTIAL, - YALE_STATE_DISARM, - AuthenticationError, - YaleSmartAlarmClient, -) from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, @@ -18,23 +11,38 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + ConfigType, + DiscoveryInfoType, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -CONF_AREA_ID = "area_id" - -DEFAULT_NAME = "Yale Smart Alarm" - -DEFAULT_AREA_ID = "1" - -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_AREA_ID, + COORDINATOR, + DEFAULT_AREA_ID, + DEFAULT_NAME, + DOMAIN, + LOGGER, + MANUFACTURER, + MODEL, + STATE_MAP, +) +from .coordinator import YaleDataUpdateCoordinator PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { @@ -46,66 +54,82 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the alarm platform.""" - name = config[CONF_NAME] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - area_id = config[CONF_AREA_ID] - - try: - client = YaleSmartAlarmClient(username, password, area_id) - except AuthenticationError: - _LOGGER.error("Authentication failed. Check credentials") - return - - add_entities([YaleAlarmDevice(name, client)], True) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Import Yale configuration from YAML.""" + LOGGER.warning( + "Loading Yale Alarm via platform setup is deprecated; Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -class YaleAlarmDevice(AlarmControlPanelEntity): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the alarm entry.""" + + async_add_entities( + [YaleAlarmDevice(coordinator=hass.data[DOMAIN][entry.entry_id][COORDINATOR])] + ) + + +class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): """Represent a Yale Smart Alarm.""" - def __init__(self, name, client): - """Initialize the Yale Alarm Device.""" - self._name = name - self._client = client - self._state = None + coordinator: YaleDataUpdateCoordinator - self._state_map = { - YALE_STATE_DISARM: STATE_ALARM_DISARMED, - YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, - YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY, - } + _attr_name: str = coordinator.entry.data[CONF_NAME] + _attr_unique_id: str = coordinator.entry.entry_id + _identifier: str = coordinator.entry.data[CONF_USERNAME] @property - def name(self): - """Return the name of the device.""" - return self._name + def device_info(self) -> DeviceInfo: + """Return device information about this entity.""" + return { + ATTR_NAME: str(self.name), + ATTR_MANUFACTURER: MANUFACTURER, + ATTR_MODEL: MODEL, + ATTR_IDENTIFIERS: {(DOMAIN, self._identifier)}, + } @property def state(self): """Return the state of the device.""" - return self._state + return STATE_MAP.get(self.coordinator.data["alarm"]) + + @property + def available(self): + """Return if entity is available.""" + return STATE_MAP.get(self.coordinator.data["alarm"]) is not None + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return False @property def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - def update(self): - """Return the state of the device.""" - armed_status = self._client.get_armed_status() - - self._state = self._state_map.get(armed_status) - def alarm_disarm(self, code=None): """Send disarm command.""" - self._client.disarm() + self.coordinator.yale.disarm() def alarm_arm_home(self, code=None): """Send arm home command.""" - self._client.arm_partial() + self.coordinator.yale.arm_partial() def alarm_arm_away(self, code=None): """Send arm away command.""" - self._client.arm_full() + self.coordinator.yale.arm_full() diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py new file mode 100644 index 00000000000..828d308b0a0 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -0,0 +1,129 @@ +"""Adds config flow for Yale Smart Alarm integration.""" +from __future__ import annotations + +import voluptuous as vol +from yalesmartalarmclient.client import AuthenticationError, YaleSmartAlarmClient + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv + +from .const import CONF_AREA_ID, DEFAULT_AREA_ID, DEFAULT_NAME, DOMAIN, LOGGER + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string, + } +) + +DATA_SCHEMA_AUTH = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) + + +class YaleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Yale integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + entry: config_entries.ConfigEntry + + async def async_step_import(self, config: dict): + """Import a configuration from config.yaml.""" + + self.context.update( + {"title_placeholders": {CONF_NAME: f"YAML import {DOMAIN}"}} + ) + return await self.async_step_user(user_input=config) + + async def async_step_reauth(self, user_input=None): + """Handle initiation of re-authentication with Yale.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + errors = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + try: + await self.hass.async_add_executor_job( + YaleSmartAlarmClient, username, password + ) + except AuthenticationError as error: + LOGGER.error("Authentication failed. Check credentials %s", error) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + errors={"base": "invalid_auth"}, + ) + + existing_entry = await self.async_set_unique_id(username) + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, + data={ + **self.entry.data, + CONF_USERNAME: username, + CONF_PASSWORD: password, + }, + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA_AUTH, + errors=errors, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + name = user_input.get(CONF_NAME, DEFAULT_NAME) + area = user_input.get(CONF_AREA_ID, DEFAULT_AREA_ID) + + try: + await self.hass.async_add_executor_job( + YaleSmartAlarmClient, username, password + ) + except AuthenticationError as error: + LOGGER.error("Authentication failed. Check credentials %s", error) + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "invalid_auth"}, + ) + + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=username, + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_NAME: name, + CONF_AREA_ID: area, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py new file mode 100644 index 00000000000..618f9ad073a --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/const.py @@ -0,0 +1,39 @@ +"""Yale integration constants.""" +import logging + +from yalesmartalarmclient.client import ( + YALE_STATE_ARM_FULL, + YALE_STATE_ARM_PARTIAL, + YALE_STATE_DISARM, +) + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) + +CONF_AREA_ID = "area_id" +DEFAULT_NAME = "Yale Smart Alarm" +DEFAULT_AREA_ID = "1" + +MANUFACTURER = "Yale" +MODEL = "main" + +DOMAIN = "yale_smart_alarm" +COORDINATOR = "coordinator" + +DEFAULT_SCAN_INTERVAL = 15 + +LOGGER = logging.getLogger(__name__) + +ATTR_ONLINE = "online" +ATTR_STATUS = "status" + +PLATFORMS = ["alarm_control_panel"] + +STATE_MAP = { + YALE_STATE_DISARM: STATE_ALARM_DISARMED, + YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, + YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY, +} diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py new file mode 100644 index 00000000000..1016f8f4d9d --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -0,0 +1,139 @@ +"""DataUpdateCoordinator for the Yale integration.""" +from __future__ import annotations + +from datetime import timedelta + +from yalesmartalarmclient.client import AuthenticationError, YaleSmartAlarmClient + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER + + +class YaleDataUpdateCoordinator(DataUpdateCoordinator): + """A Yale Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Yale hub.""" + self.entry = entry + self.yale: YaleSmartAlarmClient | None = None + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + + async def _async_update_data(self) -> dict: + """Fetch data from Yale.""" + + updates = await self.hass.async_add_executor_job(self.get_updates) + + locks = [] + door_windows = [] + + for device in updates["cycle"]["data"]["device_status"]: + state = device["status1"] + if device["type"] == "device_type.door_lock": + lock_status_str = device["minigw_lock_status"] + lock_status = int(str(lock_status_str or 0), 16) + closed = (lock_status & 16) == 16 + locked = (lock_status & 1) == 1 + if not lock_status and "device_status.lock" in state: + device["_state"] = "locked" + locks.append(device) + continue + if not lock_status and "device_status.unlock" in state: + device["_state"] = "unlocked" + locks.append(device) + continue + if ( + lock_status + and ( + "device_status.lock" in state or "device_status.unlock" in state + ) + and closed + and locked + ): + device["_state"] = "locked" + locks.append(device) + continue + if ( + lock_status + and ( + "device_status.lock" in state or "device_status.unlock" in state + ) + and closed + and not locked + ): + device["_state"] = "unlocked" + locks.append(device) + continue + if ( + lock_status + and ( + "device_status.lock" in state or "device_status.unlock" in state + ) + and not closed + ): + device["_state"] = "unlocked" + locks.append(device) + continue + device["_state"] = "unavailable" + locks.append(device) + continue + if device["type"] == "device_type.door_contact": + if "device_status.dc_close" in state: + device["_state"] = "closed" + door_windows.append(device) + continue + if "device_status.dc_open" in state: + device["_state"] = "open" + door_windows.append(device) + continue + device["_state"] = "unavailable" + door_windows.append(device) + continue + + return { + "alarm": updates["arm_status"], + "locks": locks, + "door_windows": door_windows, + "status": updates["status"], + "online": updates["online"], + } + + def get_updates(self) -> dict: + """Fetch data from Yale.""" + + if self.yale is None: + self.yale = YaleSmartAlarmClient( + self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD] + ) + + try: + arm_status = self.yale.get_armed_status() + cycle = self.yale.get_cycle() + status = self.yale.get_status() + online = self.yale.get_online() + + except AuthenticationError as error: + LOGGER.error("Authentication failed. Check credentials %s", error) + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": self.entry.entry_id}, + data=self.entry.data, + ) + ) + raise UpdateFailed from error + + return { + "arm_status": arm_status, + "cycle": cycle, + "status": status, + "online": online, + } diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index e900f4e0373..83c380881ef 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", "requirements": ["yalesmartalarmclient==0.3.3"], "codeowners": ["@gjohansson-ST"], + "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json new file mode 100644 index 00000000000..14bb48f8176 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]" + } + }, + "reauth_confirm": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]" + } + } + } + } +} diff --git a/homeassistant/components/yale_smart_alarm/translations/en.json b/homeassistant/components/yale_smart_alarm/translations/en.json new file mode 100644 index 00000000000..0c653f84e7d --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Connection already configured for this account" + }, + "error": { + "invalid_auth": "Authentication error" + }, + "step": { + "user": { + "data": { + "username": "Username", + "password": "Password", + "name": "Name of alarm", + "area_id": "Area ID" + } + }, + "reauth_confirm": { + "data": { + "username": "Usernamn", + "password": "Password", + "name": "Name of alarm", + "area_id": "Area ID" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0e7b6c52cc2..270304cb623 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -300,6 +300,7 @@ FLOWS = [ "xbox", "xiaomi_aqara", "xiaomi_miio", + "yale_smart_alarm", "yamaha_musiccast", "yeelight", "zerproc", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84b9e458b92..516e9910ae2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,6 +1324,9 @@ xknx==0.18.8 # homeassistant.components.zestimate xmltodict==0.12.0 +# homeassistant.components.yale_smart_alarm +yalesmartalarmclient==0.3.3 + # homeassistant.components.august yalexs==1.1.12 diff --git a/tests/components/yale_smart_alarm/__init__.py b/tests/components/yale_smart_alarm/__init__.py new file mode 100644 index 00000000000..472ef33a083 --- /dev/null +++ b/tests/components/yale_smart_alarm/__init__.py @@ -0,0 +1 @@ +"""Tests for the Yale Smart Living integration.""" diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py new file mode 100644 index 00000000000..142e1ac5b5d --- /dev/null +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -0,0 +1,224 @@ +"""Test the Yale Smart Living config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest +from yalesmartalarmclient.client import AuthenticationError + +from homeassistant import config_entries, setup +from homeassistant.components.yale_smart_alarm.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + ), patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + side_effect=AuthenticationError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize( + "input,output", + [ + ( + { + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + { + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + ), + ( + { + "username": "test-username", + "password": "test-password", + }, + { + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + ), + ], +) +async def test_import_flow_success(hass, input: dict[str, str], output: dict[str, str]): + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + ), patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=input, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-username" + assert result2["data"] == output + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + data={ + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + ) as mock_yale, patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "new-test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == { + "username": "test-username", + "password": "new-test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + } + + assert len(mock_yale.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + data={ + "username": "test-username", + "password": "test-password", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + side_effect=AuthenticationError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "wrong-password", + }, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} From a5eb2ac7b77e8c0df541cad0bb461585932325df Mon Sep 17 00:00:00 2001 From: G Johansson <62932417+gjohansson-ST@users.noreply.github.com> Date: Sat, 24 Jul 2021 21:13:06 +0200 Subject: [PATCH 558/818] Bump yalesmartalarmclient to 0.3.4 (#53431) --- homeassistant/components/yale_smart_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 83c380881ef..a61a1888990 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.3.3"], + "requirements": ["yalesmartalarmclient==0.3.4"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index a25df049f64..d8ea2df8be5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2410,7 +2410,7 @@ xmltodict==0.12.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.3 +yalesmartalarmclient==0.3.4 # homeassistant.components.august yalexs==1.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 516e9910ae2..0ca209f342e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ xknx==0.18.8 xmltodict==0.12.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.3 +yalesmartalarmclient==0.3.4 # homeassistant.components.august yalexs==1.1.12 From 68bf6194e122ef012422e8bb031c602c32763f5f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 24 Jul 2021 22:45:24 +0300 Subject: [PATCH 559/818] Add myself to webOS TV codeowners (#53428) --- CODEOWNERS | 2 +- homeassistant/components/webostv/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0e92885d247..3979a3e4453 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -561,7 +561,7 @@ homeassistant/components/wallbox/* @hesselonline homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff -homeassistant/components/webostv/* @bendavid +homeassistant/components/webostv/* @bendavid @thecode homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @esev homeassistant/components/wiffi/* @mampfes diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index b14fd793cab..9697f903926 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": ["aiopylgtv==0.4.0"], "dependencies": ["configurator"], - "codeowners": ["@bendavid"], + "codeowners": ["@bendavid", "@thecode"], "iot_class": "local_polling" } From c1b18f48678dd40a333ea05a588be1caf5b2a050 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 25 Jul 2021 00:12:45 +0000 Subject: [PATCH 560/818] [ci skip] Translation update --- .../components/adax/translations/he.json | 20 ++++++++ .../components/adax/translations/nl.json | 20 ++++++++ .../components/automate/translations/he.json | 19 +++++++ .../automate/translations/zh-Hant.json | 19 +++++++ .../components/co2signal/translations/he.json | 30 +++++++++++ .../components/flipr/translations/he.json | 15 ++++++ .../growatt_server/translations/he.json | 1 + .../growatt_server/translations/nl.json | 1 + .../components/honeywell/translations/he.json | 15 ++++++ .../components/litejet/translations/ca.json | 10 ++++ .../components/litejet/translations/et.json | 10 ++++ .../components/litejet/translations/nl.json | 10 ++++ .../components/litejet/translations/pl.json | 10 ++++ .../components/litejet/translations/ru.json | 10 ++++ .../nfandroidtv/translations/he.json | 19 +++++++ .../nfandroidtv/translations/nl.json | 20 ++++++++ .../switcher_kis/translations/he.json | 13 +++++ .../yale_smart_alarm/translations/en.json | 50 +++++++++---------- 18 files changed, 267 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/adax/translations/he.json create mode 100644 homeassistant/components/adax/translations/nl.json create mode 100644 homeassistant/components/automate/translations/he.json create mode 100644 homeassistant/components/automate/translations/zh-Hant.json create mode 100644 homeassistant/components/co2signal/translations/he.json create mode 100644 homeassistant/components/flipr/translations/he.json create mode 100644 homeassistant/components/honeywell/translations/he.json create mode 100644 homeassistant/components/nfandroidtv/translations/he.json create mode 100644 homeassistant/components/nfandroidtv/translations/nl.json create mode 100644 homeassistant/components/switcher_kis/translations/he.json diff --git a/homeassistant/components/adax/translations/he.json b/homeassistant/components/adax/translations/he.json new file mode 100644 index 00000000000..54d31cd2669 --- /dev/null +++ b/homeassistant/components/adax/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "account_id": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df", + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/nl.json b/homeassistant/components/adax/translations/nl.json new file mode 100644 index 00000000000..2bec9774c0a --- /dev/null +++ b/homeassistant/components/adax/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "account_id": "Account ID", + "host": "Host", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/he.json b/homeassistant/components/automate/translations/he.json new file mode 100644 index 00000000000..accc8a0a610 --- /dev/null +++ b/homeassistant/components/automate/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/zh-Hant.json b/homeassistant/components/automate/translations/zh-Hant.json new file mode 100644 index 00000000000..0fbaa19828f --- /dev/null +++ b/homeassistant/components/automate/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/he.json b/homeassistant/components/co2signal/translations/he.json new file mode 100644 index 00000000000..9ff327c584b --- /dev/null +++ b/homeassistant/components/co2signal/translations/he.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + }, + "country": { + "data": { + "country_code": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05de\u05d3\u05d9\u05e0\u05d4" + } + }, + "user": { + "data": { + "api_key": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/he.json b/homeassistant/components/flipr/translations/he.json new file mode 100644 index 00000000000..85872f14f2c --- /dev/null +++ b/homeassistant/components/flipr/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/he.json b/homeassistant/components/growatt_server/translations/he.json index cde5cec4fa4..14b47532075 100644 --- a/homeassistant/components/growatt_server/translations/he.json +++ b/homeassistant/components/growatt_server/translations/he.json @@ -8,6 +8,7 @@ "data": { "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/growatt_server/translations/nl.json b/homeassistant/components/growatt_server/translations/nl.json index 86b5a98b131..8d27f2b22af 100644 --- a/homeassistant/components/growatt_server/translations/nl.json +++ b/homeassistant/components/growatt_server/translations/nl.json @@ -17,6 +17,7 @@ "data": { "name": "Naam", "password": "Wachtwoord", + "url": "URL", "username": "Gebruikersnaam" }, "title": "Vul uw Growatt gegevens in" diff --git a/homeassistant/components/honeywell/translations/he.json b/homeassistant/components/honeywell/translations/he.json new file mode 100644 index 00000000000..fc7a38e2658 --- /dev/null +++ b/homeassistant/components/honeywell/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ca.json b/homeassistant/components/litejet/translations/ca.json index 39e2a56dc4d..2b301a256db 100644 --- a/homeassistant/components/litejet/translations/ca.json +++ b/homeassistant/components/litejet/translations/ca.json @@ -15,5 +15,15 @@ "title": "Connexi\u00f3 amb LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transici\u00f3 predeterminada (segons)" + }, + "title": "Configuraci\u00f3 de LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/et.json b/homeassistant/components/litejet/translations/et.json index 6e50b5dcdf3..ef6666ab025 100644 --- a/homeassistant/components/litejet/translations/et.json +++ b/homeassistant/components/litejet/translations/et.json @@ -15,5 +15,15 @@ "title": "Loo \u00fchendus LiteJetiga" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)" + }, + "title": "LiteJeti seadistamine" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/nl.json b/homeassistant/components/litejet/translations/nl.json index a96f8de6171..e2743b4165f 100644 --- a/homeassistant/components/litejet/translations/nl.json +++ b/homeassistant/components/litejet/translations/nl.json @@ -15,5 +15,15 @@ "title": "Maak verbinding met LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Standaard overgang (seconden)" + }, + "title": "Configureer LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/pl.json b/homeassistant/components/litejet/translations/pl.json index 20e5d68288d..ae26b5e7283 100644 --- a/homeassistant/components/litejet/translations/pl.json +++ b/homeassistant/components/litejet/translations/pl.json @@ -15,5 +15,15 @@ "title": "Po\u0142\u0105czenie z LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Domy\u015blny czas efektu przej\u015bcia (w sekundach)" + }, + "title": "Konfiguracja LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ru.json b/homeassistant/components/litejet/translations/ru.json index c90e6956301..b6ad0e913e9 100644 --- a/homeassistant/components/litejet/translations/ru.json +++ b/homeassistant/components/litejet/translations/ru.json @@ -15,5 +15,15 @@ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "\u041f\u0435\u0440\u0435\u0445\u043e\u0434 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/he.json b/homeassistant/components/nfandroidtv/translations/he.json new file mode 100644 index 00000000000..70ced66b0a5 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/nl.json b/homeassistant/components/nfandroidtv/translations/nl.json new file mode 100644 index 00000000000..b231bd00c3c --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam" + }, + "title": "Meldingen voor Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/he.json b/homeassistant/components/switcher_kis/translations/he.json new file mode 100644 index 00000000000..d3d68dccc93 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/en.json b/homeassistant/components/yale_smart_alarm/translations/en.json index 0c653f84e7d..84cfb893ad5 100644 --- a/homeassistant/components/yale_smart_alarm/translations/en.json +++ b/homeassistant/components/yale_smart_alarm/translations/en.json @@ -1,28 +1,28 @@ { - "config": { - "abort": { - "already_configured": "Connection already configured for this account" - }, - "error": { - "invalid_auth": "Authentication error" - }, - "step": { - "user": { - "data": { - "username": "Username", - "password": "Password", - "name": "Name of alarm", - "area_id": "Area ID" + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Password", + "username": "Username" + } + }, + "user": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Password", + "username": "Username" + } + } } - }, - "reauth_confirm": { - "data": { - "username": "Usernamn", - "password": "Password", - "name": "Name of alarm", - "area_id": "Area ID" - } - } } - } -} +} \ No newline at end of file From 7e59f3160bf91a33c857266b961f77dadc2d2497 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Jul 2021 02:19:08 -0400 Subject: [PATCH 561/818] Use entity class attributes for climacell (#53444) --- homeassistant/components/climacell/weather.py | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 2043459c496..865c2baa330 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -127,24 +127,11 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Initialize ClimaCell Weather Entity.""" super().__init__(config_entry, coordinator, api_version) self.forecast_type = forecast_type - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - if self.forecast_type == DEFAULT_FORECAST_TYPE: - return True - - return False - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}" - - @property - def unique_id(self) -> str: - """Return the unique id of the entity.""" - return f"{self._config_entry.unique_id}_{self.forecast_type}" + self._attr_entity_registry_enabled_default = ( + forecast_type == DEFAULT_FORECAST_TYPE + ) + self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}" + self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}" @staticmethod @abstractmethod @@ -274,6 +261,8 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): """Entity that talks to ClimaCell v4 API to retrieve weather data.""" + _attr_temperature_unit = TEMP_FAHRENHEIT + @staticmethod def _translate_condition( condition: int | None, sun_is_up: bool = True @@ -294,11 +283,6 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): """Return the platform temperature.""" return self._get_current_property(CC_ATTR_TEMPERATURE) - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - @property def _pressure(self): """Return the raw pressure.""" @@ -424,6 +408,8 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): """Entity that talks to ClimaCell v3 API to retrieve weather data.""" + _attr_temperature_unit = TEMP_FAHRENHEIT + @staticmethod def _translate_condition( condition: str | None, sun_is_up: bool = True @@ -444,11 +430,6 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE ) - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - @property def _pressure(self): """Return the raw pressure.""" From c8d2fc1e04e15b4e61b1c78adebb1ff23fff5b30 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 25 Jul 2021 11:31:16 +0100 Subject: [PATCH 562/818] Fix System Bridge unique key for filesystem sensors (#53446) --- .../components/system_bridge/sensor.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 71bb7030a11..ea7fc628e76 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -223,25 +223,26 @@ class BridgeFilesystemSensor(BridgeSensor): self, coordinator: DataUpdateCoordinator, bridge: Bridge, key: str ) -> None: """Initialize System Bridge sensor.""" + uid_key = key.replace(":", "") super().__init__( coordinator, bridge, - f"filesystem_{key}", + f"filesystem_{uid_key}", f"{key} Space Used", "mdi:harddisk", None, PERCENTAGE, True, ) - self._key = key + self._fs_key = key @property def state(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( - round(bridge.filesystem.fsSize[self._key]["use"], 2) - if bridge.filesystem.fsSize[self._key]["use"] is not None + round(bridge.filesystem.fsSize[self._fs_key]["use"], 2) + if bridge.filesystem.fsSize[self._fs_key]["use"] is not None else None ) @@ -250,12 +251,12 @@ class BridgeFilesystemSensor(BridgeSensor): """Return the state attributes of the entity.""" bridge: Bridge = self.coordinator.data return { - ATTR_AVAILABLE: bridge.filesystem.fsSize[self._key]["available"], - ATTR_FILESYSTEM: bridge.filesystem.fsSize[self._key]["fs"], - ATTR_MOUNT: bridge.filesystem.fsSize[self._key]["mount"], - ATTR_SIZE: bridge.filesystem.fsSize[self._key]["size"], - ATTR_TYPE: bridge.filesystem.fsSize[self._key]["type"], - ATTR_USED: bridge.filesystem.fsSize[self._key]["used"], + ATTR_AVAILABLE: bridge.filesystem.fsSize[self._fs_key]["available"], + ATTR_FILESYSTEM: bridge.filesystem.fsSize[self._fs_key]["fs"], + ATTR_MOUNT: bridge.filesystem.fsSize[self._fs_key]["mount"], + ATTR_SIZE: bridge.filesystem.fsSize[self._fs_key]["size"], + ATTR_TYPE: bridge.filesystem.fsSize[self._fs_key]["type"], + ATTR_USED: bridge.filesystem.fsSize[self._fs_key]["used"], } From ff8affdd049797d364085b391673f8f078cbce75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 25 Jul 2021 12:32:50 +0200 Subject: [PATCH 563/818] Address late review of Adax (#53456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/adax/config_flow.py | 4 +++- homeassistant/components/adax/strings.json | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/adax/config_flow.py b/homeassistant/components/adax/config_flow.py index 166278ef48d..cf845df5e06 100644 --- a/homeassistant/components/adax/config_flow.py +++ b/homeassistant/components/adax/config_flow.py @@ -52,8 +52,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} + await self.async_set_unique_id(user_input[ACCOUNT_ID]) + self._abort_if_unique_id_configured() + try: - self._async_abort_entries_match({ACCOUNT_ID: user_input[ACCOUNT_ID]}) await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/adax/strings.json b/homeassistant/components/adax/strings.json index 0f7aac83f5a..213e1f95cf9 100644 --- a/homeassistant/components/adax/strings.json +++ b/homeassistant/components/adax/strings.json @@ -3,15 +3,13 @@ "step": { "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", "account_id": "Account ID", "password": "[%key:common::config_flow::data::password%]" } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" From 33af2602af85af6fbe1605d110530ec50d23daad Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 25 Jul 2021 19:43:31 +0200 Subject: [PATCH 564/818] Fix wan/device uptime and add state_class to counters for Fritz (#52574) * Fix wan/device uptime, add state_class to counters * Rebase + cleanup + adapt to final fritzconnection * Bump fritzconnection library * Missing bump --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritz/sensor.py | 50 +++++++++++++++---- .../fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index a7c51a69cf7..46531183afd 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==1.4.2", + "fritzconnection==1.6.0", "xmltodict==0.12.0" ], "dependencies": ["network"], diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 215848251e9..482d5b1d688 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -26,9 +26,9 @@ from .const import DOMAIN, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) -def _retrieve_uptime_state(status: FritzStatus, last_value: str) -> str: - """Return uptime from device.""" - delta_uptime = utcnow() - datetime.timedelta(seconds=status.uptime) +def _uptime_calculation(seconds_uptime: float, last_value: str | None) -> str: + """Calculate uptime with deviation.""" + delta_uptime = utcnow() - datetime.timedelta(seconds=seconds_uptime) if ( not last_value @@ -42,6 +42,18 @@ def _retrieve_uptime_state(status: FritzStatus, last_value: str) -> str: return last_value +def _retrieve_device_uptime_state(status: FritzStatus, last_value: str) -> str: + """Return uptime from device.""" + return _uptime_calculation(status.device_uptime, last_value) + + +def _retrieve_connection_uptime_state( + status: FritzStatus, last_value: str | None +) -> str: + """Return uptime from connection.""" + return _uptime_calculation(status.connection_uptime, last_value) + + def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: """Return external ip from device.""" return status.external_ip # type: ignore[no-any-return] @@ -83,6 +95,7 @@ class SensorData(TypedDict, total=False): name: str device_class: str | None state_class: str | None + last_reset: bool unit_of_measurement: str | None icon: str | None state_provider: Callable @@ -94,10 +107,15 @@ SENSOR_DATA = { icon="mdi:earth", state_provider=_retrieve_external_ip_state, ), - "uptime": SensorData( - name="Uptime", + "device_uptime": SensorData( + name="Device Uptime", device_class=DEVICE_CLASS_TIMESTAMP, - state_provider=_retrieve_uptime_state, + state_provider=_retrieve_device_uptime_state, + ), + "connection_uptime": SensorData( + name="Connection Uptime", + device_class=DEVICE_CLASS_TIMESTAMP, + state_provider=_retrieve_connection_uptime_state, ), "kb_s_sent": SensorData( name="kB/s sent", @@ -127,12 +145,16 @@ SENSOR_DATA = { ), "gb_sent": SensorData( name="GB sent", + state_class=STATE_CLASS_MEASUREMENT, + last_reset=True, unit_of_measurement=DATA_GIGABYTES, icon="mdi:upload", state_provider=_retrieve_gb_sent_state, ), "gb_received": SensorData( name="GB received", + state_class=STATE_CLASS_MEASUREMENT, + last_reset=True, unit_of_measurement=DATA_GIGABYTES, icon="mdi:download", state_provider=_retrieve_gb_received_state, @@ -170,7 +192,8 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): ) -> None: """Init FRITZ!Box connectivity class.""" self._sensor_data: SensorData = SENSOR_DATA[sensor_type] - self._last_value: str | None = None + self._last_device_value: str | None = None + self._last_wan_value: str | None = None self._attr_available = True self._attr_device_class = self._sensor_data.get("device_class") self._attr_icon = self._sensor_data.get("icon") @@ -197,6 +220,15 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): self._attr_available = False return - self._attr_state = self._last_value = self._state_provider( - status, self._last_value + self._attr_state = self._last_device_value = self._state_provider( + status, self._last_device_value ) + + if self._sensor_data.get("last_reset") is True: + self._last_wan_value = _retrieve_connection_uptime_state( + status, self._last_wan_value + ) + self._attr_last_reset = datetime.datetime.strptime( + self._last_wan_value, + "%Y-%m-%dT%H:%M:%S%z", + ) diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 531fa13e232..0a1f7330c6d 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.4.2"], + "requirements": ["fritzconnection==1.6.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d8ea2df8be5..84cfa5f0cf7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.4.2 +fritzconnection==1.6.0 # homeassistant.components.google_translate gTTS==2.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ca209f342e..2f9828c479d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -358,7 +358,7 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.4.2 +fritzconnection==1.6.0 # homeassistant.components.google_translate gTTS==2.2.3 From 5189172b80d9d72250fc2aa91734a3af09cf58a6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 25 Jul 2021 10:58:50 -0700 Subject: [PATCH 565/818] Pass clientsession. (#53455) --- homeassistant/components/motioneye/__init__.py | 2 ++ homeassistant/components/motioneye/config_flow.py | 2 ++ homeassistant/components/motioneye/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 282a24fae40..78f9fd7a384 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -47,6 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -274,6 +275,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: admin_password=entry.data.get(CONF_ADMIN_PASSWORD), surveillance_username=entry.data.get(CONF_SURVEILLANCE_USERNAME), surveillance_password=entry.data.get(CONF_SURVEILLANCE_PASSWORD), + session=async_get_clientsession(hass), ) try: diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index d6792bba2a8..a5f92a3ce09 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.const import CONF_SOURCE, CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import create_motioneye_client from .const import ( @@ -114,6 +115,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): admin_password=user_input.get(CONF_ADMIN_PASSWORD), surveillance_username=user_input.get(CONF_SURVEILLANCE_USERNAME), surveillance_password=user_input.get(CONF_SURVEILLANCE_PASSWORD), + session=async_get_clientsession(self.hass), ) errors = {} diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 4d1863c8e6a..59c09097223 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -8,7 +8,7 @@ "webhook" ], "requirements": [ - "motioneye-client==0.3.9" + "motioneye-client==0.3.10" ], "codeowners": [ "@dermotduffy" diff --git a/requirements_all.txt b/requirements_all.txt index 84cfa5f0cf7..70e3cf94b2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -985,7 +985,7 @@ mitemp_bt==0.0.3 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.9 +motioneye-client==0.3.10 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f9828c479d..c6a6b938444 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -557,7 +557,7 @@ minio==4.0.9 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.9 +motioneye-client==0.3.10 # homeassistant.components.mullvad mullvad-api==1.0.0 From d18ca62f6070c54b9023221f21c1d750711770e3 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 25 Jul 2021 19:23:38 +0100 Subject: [PATCH 566/818] Bump aioambient to 1.2.4 (#53467) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 6d4c40d260d..42b22d26a10 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.2.4"], + "requirements": ["aioambient==1.2.5"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 70e3cf94b2e..7316432de01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==1.2.4 +aioambient==1.2.5 # homeassistant.components.asuswrt aioasuswrt==1.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6a6b938444..314ddfee902 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==1.2.4 +aioambient==1.2.5 # homeassistant.components.asuswrt aioasuswrt==1.3.4 From 5741a59d08fc0e9fcec7c96221754fb017864a67 Mon Sep 17 00:00:00 2001 From: G Johansson <62932417+gjohansson-ST@users.noreply.github.com> Date: Sun, 25 Jul 2021 21:17:45 +0200 Subject: [PATCH 567/818] Bugfix package 0.3.4 (#53470) --- homeassistant/components/yale_smart_alarm/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 1016f8f4d9d..8b09507e956 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -35,7 +35,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): locks = [] door_windows = [] - for device in updates["cycle"]["data"]["device_status"]: + for device in updates["cycle"]["device_status"]: state = device["status1"] if device["type"] == "device_type.door_lock": lock_status_str = device["minigw_lock_status"] From 5e6853b9e18fcad1137140843af3a439a80c10eb Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 25 Jul 2021 12:23:11 -0700 Subject: [PATCH 568/818] Codereview fixes. (#53452) --- homeassistant/components/motioneye/switch.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 08d58a9f6a1..4abf00969c9 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations from types import MappingProxyType -from typing import Any, Callable +from typing import Any from motioneye_client.client import MotionEyeClient from motioneye_client.const import ( @@ -18,6 +18,7 @@ from motioneye_client.const import ( from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import MotionEyeEntity, get_camera_from_cameras, listen_for_new_cameras @@ -34,8 +35,8 @@ MOTIONEYE_SWITCHES = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable -) -> bool: + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up motionEye from a config entry.""" entry_data = hass.data[DOMAIN][entry.entry_id] @@ -59,7 +60,6 @@ async def async_setup_entry( ) listen_for_new_cameras(hass, entry, camera_add) - return True class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): @@ -79,8 +79,7 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): """Initialize the switch.""" self._switch_key = switch_key self._switch_key_friendly_name = switch_key_friendly_name - MotionEyeEntity.__init__( - self, + super().__init__( config_entry_id, f"{TYPE_MOTIONEYE_SWITCH_BASE}_{switch_key}", camera, @@ -93,8 +92,8 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): @property def name(self) -> str: """Return the name of the switch.""" - camera_name = self._camera[KEY_NAME] if self._camera else "" - return f"{camera_name} {self._switch_key_friendly_name}" + camera_prepend = f"{self._camera[KEY_NAME]} " if self._camera else "" + return f"{camera_prepend}{self._switch_key_friendly_name}" @property def is_on(self) -> bool: From f3ba71748b5d4ce9211adb05c493ab3613c8182c Mon Sep 17 00:00:00 2001 From: David Kendall Date: Sun, 25 Jul 2021 20:33:21 +0100 Subject: [PATCH 569/818] Feature/google calendar read only support (#52790) * feat(google): Added support for read only access in google calendar. By default the current read/write access will be given, but the user has the option to set read only access which stops the add event service from registering * fix(google): Updated documentation link * docs(google-calendar): Added style fixes * feat(calendar-google): Updated scopes to be defined on enum property. This was done as a MR suggestion to simplify the code. * feat(calendar-google):Removed constants no longer needed. * feat(calendar-google): Reduced scope check to minimum. * style: Fixed style issues --- homeassistant/components/google/__init__.py | 51 +++++++++++++++---- homeassistant/components/google/manifest.json | 2 +- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index b929c3c4c37..5ed7bc93b78 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,5 +1,6 @@ """Support for Google - Calendar Event Devices.""" from datetime import datetime, timedelta +from enum import Enum import logging import os @@ -41,6 +42,7 @@ CONF_TRACK = "track" CONF_SEARCH = "search" CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_MAX_RESULTS = "max_results" +CONF_CALENDAR_ACCESS = "calendar_access" DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_OFFSET = "!!" @@ -70,10 +72,26 @@ SERVICE_ADD_EVENT = "add_event" DATA_INDEX = "google_calendars" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" -SCOPES = "https://www.googleapis.com/auth/calendar" TOKEN_FILE = f".{DOMAIN}.token" + +class FeatureAccess(Enum): + """Class to represent different access scopes.""" + + read_only = "https://www.googleapis.com/auth/calendar.readonly" + read_write = "https://www.googleapis.com/auth/calendar" + + def __init__(self, scope: str) -> None: + """Init instance.""" + self._scope = scope + + @property + def scope(self) -> str: + """Google calendar scope for the feature.""" + return self._scope + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -81,6 +99,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( + FeatureAccess + ), } ) }, @@ -139,7 +160,7 @@ def do_authentication(hass, hass_config, config): oauth = OAuth2WebServerFlow( client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_CLIENT_SECRET], - scope="https://www.googleapis.com/auth/calendar", + scope=config[CONF_CALENDAR_ACCESS].scope, redirect_uri="Home-Assistant.io", ) try: @@ -213,7 +234,7 @@ def setup(hass, config): if not os.path.isfile(token_file): do_authentication(hass, config, conf) else: - if not check_correct_scopes(token_file): + if not check_correct_scopes(token_file, conf): do_authentication(hass, config, conf) else: do_setup(hass, config, conf) @@ -221,16 +242,22 @@ def setup(hass, config): return True -def check_correct_scopes(token_file): +def check_correct_scopes(token_file, config): """Check for the correct scopes in file.""" with open(token_file) as tokenfile: - if "readonly" in tokenfile.read(): + contents = tokenfile.read() + + # Check for quoted scope as our scopes can be subsets of other scopes + target_scope = f'"{config.get(CONF_CALENDAR_ACCESS).scope}"' + if target_scope not in contents: _LOGGER.warning("Please re-authenticate with Google") return False return True -def setup_services(hass, hass_config, track_new_found_calendars, calendar_service): +def setup_services( + hass, hass_config, config, track_new_found_calendars, calendar_service +): """Set up the service listeners.""" def _found_calendar(call): @@ -312,9 +339,11 @@ def setup_services(hass, hass_config, track_new_found_calendars, calendar_servic service_data = {"calendarId": call.data[EVENT_CALENDAR_ID], "body": event} event = service.events().insert(**service_data).execute() - hass.services.register( - DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA - ) + # Only expose the add event service if we have the correct permissions + if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write: + hass.services.register( + DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA + ) return True @@ -327,7 +356,9 @@ def do_setup(hass, hass_config, config): track_new_found_calendars = convert( config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW ) - setup_services(hass, hass_config, track_new_found_calendars, calendar_service) + setup_services( + hass, hass_config, config, track_new_found_calendars, calendar_service + ) for calendar in hass.data[DATA_INDEX].values(): discovery.load_platform(hass, "calendar", DOMAIN, calendar, hass_config) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 9b6f7d77f26..e96cf4ec0c6 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,7 +1,7 @@ { "domain": "google", "name": "Google Calendars", - "documentation": "https://www.home-assistant.io/integrations/google", + "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": [ "google-api-python-client==1.6.4", "httplib2==0.19.0", From 15ec9fbf6c2585c9a2e883be6c1f9e1822382198 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 26 Jul 2021 00:59:14 +0200 Subject: [PATCH 570/818] Late review comment in edl21 (#53464) * Late review comment. * Review comment. * pylint. * Update homeassistant/components/edl21/sensor.py * Callback typing. * Complete typing Co-authored-by: Martin Hjelmare --- homeassistant/components/edl21/sensor.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 65ca1ee9050..d2d0d375733 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -10,13 +10,15 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -35,7 +37,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the EDL21 sensor.""" hass.data[DOMAIN] = EDL21(hass, config, async_add_entities) await hass.data[DOMAIN].connect() @@ -126,7 +133,7 @@ class EDL21: def __init__(self, hass, config, async_add_entities) -> None: """Initialize an EDL21 object.""" - self._registered_obis: set[()] = set() + self._registered_obis: set[tuple[str, str]] = set() self._hass = hass self._async_add_entities = async_add_entities self._name = config[CONF_NAME] From c6ee058c0df872debc32b2e435e6e1a0fdeafa72 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Jul 2021 00:10:23 +0000 Subject: [PATCH 571/818] [ci skip] Translation update --- .../litejet/translations/zh-Hant.json | 10 +++++++ .../yale_smart_alarm/translations/ca.json | 28 +++++++++++++++++++ .../yale_smart_alarm/translations/et.json | 28 +++++++++++++++++++ .../yale_smart_alarm/translations/ru.json | 28 +++++++++++++++++++ .../translations/zh-Hant.json | 28 +++++++++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 homeassistant/components/yale_smart_alarm/translations/ca.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/et.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/ru.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/zh-Hant.json diff --git a/homeassistant/components/litejet/translations/zh-Hant.json b/homeassistant/components/litejet/translations/zh-Hant.json index 8a268f3db49..3e6886e74a3 100644 --- a/homeassistant/components/litejet/translations/zh-Hant.json +++ b/homeassistant/components/litejet/translations/zh-Hant.json @@ -15,5 +15,15 @@ "title": "\u9023\u7dda\u81f3 LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "\u9810\u8a2d\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09" + }, + "title": "\u8a2d\u5b9a LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ca.json b/homeassistant/components/yale_smart_alarm/translations/ca.json new file mode 100644 index 00000000000..f3865103996 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, + "user": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/et.json b/homeassistant/components/yale_smart_alarm/translations/et.json new file mode 100644 index 00000000000..e773e628d1e --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Tsooni ID", + "name": "Nimi", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, + "user": { + "data": { + "area_id": "Tsooni ID", + "name": "Nimi", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ru.json b/homeassistant/components/yale_smart_alarm/translations/ru.json new file mode 100644 index 00000000000..03af44dd983 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "user": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json new file mode 100644 index 00000000000..6cfcaf7c83b --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "user": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file From 505dd500cbc761895eea9bccfa64a6408d2b8d50 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 25 Jul 2021 22:09:40 -0400 Subject: [PATCH 572/818] Bump up ZHA dependencies (#53472) * Bump up ZHA dependencies * Fix ZHA WS API tests --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- tests/components/zha/test_api.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 081941d94fe..c8df2ddeb30 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,16 +4,16 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.25.0", + "bellows==0.26.0", "pyserial==3.5", "pyserial-asyncio==0.5", "zha-quirks==0.0.59", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.35.2", + "zigpy==0.36.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.5.1" + "zigpy-znp==0.5.2" ], "codeowners": ["@dmulcahey", "@adminiuga"], "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7316432de01..523bca15f47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -364,7 +364,7 @@ beautifulsoup4==4.9.3 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.25.0 +bellows==0.26.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 @@ -2455,10 +2455,10 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.1 +zigpy-znp==0.5.2 # homeassistant.components.zha -zigpy==0.35.2 +zigpy==0.36.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 314ddfee902..e2fd0df2fe0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -220,7 +220,7 @@ azure-eventhub==5.5.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.25.0 +bellows==0.26.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 @@ -1355,10 +1355,10 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.1 +zigpy-znp==0.5.2 # homeassistant.components.zha -zigpy==0.35.2 +zigpy==0.36.0 # homeassistant.components.zwave_js zwave-js-server-python==0.28.0 diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 8694b59ecfb..288f886a865 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -136,7 +136,7 @@ async def test_device_cluster_attributes(zha_client): msg = await zha_client.receive_json() attributes = msg["result"] - assert len(attributes) == 4 + assert len(attributes) == 5 for attribute in attributes: assert attribute[ID] is not None From c18b626d67fe07d532484dc82f03b063ac399868 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Jul 2021 23:14:38 -0500 Subject: [PATCH 573/818] Fix flakey august pubnub test (#53474) --- tests/components/august/test_lock.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index a0b44d4fb79..9d1c34d917a 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -290,6 +290,8 @@ async def test_lock_update_via_pubnub(hass): ) await hass.async_block_till_done() + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_UNLOCKING @@ -297,7 +299,7 @@ async def test_lock_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=(dt_util.utcnow().timestamp() + 1) * 10000000, message={ "status": "kAugLockState_Locking", }, @@ -305,6 +307,8 @@ async def test_lock_update_via_pubnub(hass): ) await hass.async_block_till_done() + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_LOCKING @@ -329,13 +333,15 @@ async def test_lock_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000, message={ "status": "kAugLockState_Unlocking", }, ), ) await hass.async_block_till_done() + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_UNLOCKING From 550a6f159e72fbf143a3de781678525aab779ea0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 26 Jul 2021 00:54:38 -0500 Subject: [PATCH 574/818] Reduce repetitive noise in Sonos debug logs (#53352) --- homeassistant/components/sonos/config_flow.py | 6 ------ homeassistant/components/sonos/speaker.py | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 762dbc5f0ee..3bbdf2d9a26 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -42,12 +42,6 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): boot_seqnum = properties.get("bootseq") model = properties.get("model") uid = hostname_to_uid(hostname) - _LOGGER.debug( - "Calling async_discovered_player for %s with uid=%s and boot_seqnum=%s", - host, - uid, - boot_seqnum, - ) if discovery_manager := self.hass.data.get(DATA_SONOS_DISCOVERY_MANAGER): discovery_manager.async_discovered_player( "Zeroconf", properties, host, uid, boot_seqnum, model diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index af697836908..2ebef334873 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -460,7 +460,6 @@ class SonosSpeaker: self.soco = soco was_available = self.available - _LOGGER.debug("Async seen: %s, was_available: %s", self.soco, was_available) if self._seen_timer: self._seen_timer() @@ -473,6 +472,12 @@ class SonosSpeaker: self.async_write_entity_states() return + _LOGGER.debug( + "%s [%s] was not available, setting up", + self.zone_name, + self.soco.ip_address, + ) + self._poll_timer = self.hass.helpers.event.async_track_time_interval( partial( async_dispatcher_send, From 3b9608571656a4f360ef1a8d8e3297b22b9cf874 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jul 2021 08:44:32 +0200 Subject: [PATCH 575/818] Bump codecov/codecov-action from 2.0.1 to 2.0.2 (#53487) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.0.1 to 2.0.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2.0.1...v2.0.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8e03a527e74..06d22228a28 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -740,4 +740,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2.0.1 + uses: codecov/codecov-action@v2.0.2 From 25229a967086ef7143bd3be3e1f72d94ec2cc942 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jul 2021 02:27:49 -0500 Subject: [PATCH 576/818] Allow zeroconf name change if there is another Home Assistant running on the local network (#53476) * Allow zeroconf name change if there is another Home Assistant running on the local network * Remove unused try/except --- homeassistant/components/zeroconf/__init__.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 907ec680cb4..a19da8df75f 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -11,12 +11,7 @@ import socket from typing import Any, TypedDict, cast import voluptuous as vol -from zeroconf import ( - InterfaceChoice, - IPVersion, - NonUniqueNameException, - ServiceStateChange, -) +from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange from zeroconf.asyncio import AsyncServiceInfo from homeassistant import config_entries @@ -242,12 +237,7 @@ async def _async_register_hass_zc_service( ) _LOGGER.info("Starting Zeroconf broadcast") - try: - await aio_zc.async_register_service(info) - except NonUniqueNameException: - _LOGGER.error( - "Home Assistant instance with identical name present in the local network" - ) + await aio_zc.async_register_service(info, allow_name_change=True) class FlowDispatcher: From 51e4f66b82f1c7e87f2197b01709c1674329dd73 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 26 Jul 2021 09:33:06 +0200 Subject: [PATCH 577/818] Fix ESPHome services when .storage cleared (#53488) --- homeassistant/components/esphome/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0db72ad8f3b..e4976202983 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -653,6 +653,9 @@ async def _register_service( async def _setup_services( hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService] ) -> None: + if entry_data.device_info is None: + # Can happen if device has never connected or .storage cleared + return old_services = entry_data.services.copy() to_unregister = [] to_register = [] @@ -673,7 +676,6 @@ async def _setup_services( entry_data.services = {serv.key: serv for serv in services} - assert entry_data.device_info is not None for service in to_unregister: service_name = f"{entry_data.device_info.name}_{service.name}" hass.services.async_remove(DOMAIN, service_name) From fbe576e93ad33bb1f2096ea6b47129c36dc3c5c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Jul 2021 11:01:02 +0200 Subject: [PATCH 578/818] Deprecate wled.preset service (#53383) --- homeassistant/components/wled/light.py | 8 ++++++++ homeassistant/components/wled/services.yaml | 5 ++--- tests/components/wled/test_light.py | 7 ++++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index f4251a90343..403aa5e7368 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -36,6 +36,7 @@ from .const import ( ATTR_SEGMENT_ID, ATTR_SPEED, DOMAIN, + LOGGER, SERVICE_EFFECT, SERVICE_PRESET, ) @@ -164,6 +165,13 @@ class WLEDMasterLight(WLEDEntity, LightEntity): preset: int, ) -> None: """Set a WLED light to a saved preset.""" + # The WLED preset service is replaced by a preset select entity + # and marked deprecated as of Home Assistant 2021.8 + LOGGER.warning( + "The 'wled.preset' service is deprecated and replaced by a " + "dedicated preset select entity; Please use that entity to " + "change presets instead" + ) await self.coordinator.wled.preset(preset=preset) diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index f8d636686be..9ca73fac0a3 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -34,14 +34,13 @@ effect: max: 255 reverse: name: Reverse effect - description: - Reverse the effect. Either true to reverse or false otherwise. + description: Reverse the effect. Either true to reverse or false otherwise. default: false selector: boolean: preset: - name: Set preset + name: Set preset (deprecated) description: Set a preset for the WLED device. target: entity: diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index d61b675e2f2..14166e1956e 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -566,7 +566,10 @@ async def test_effect_service_error( async def test_preset_service( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the preset service of a WLED light.""" await hass.services.async_call( @@ -595,6 +598,8 @@ async def test_preset_service( assert mock_wled.preset.call_count == 2 mock_wled.preset.assert_called_with(preset=2) + assert "The 'wled.preset' service is deprecated" in caplog.text + async def test_preset_service_error( hass: HomeAssistant, From 3a5347f69e986c8af197a95279b752b0701ca335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 26 Jul 2021 11:01:58 +0200 Subject: [PATCH 579/818] Handle serverTime change (#53490) --- homeassistant/components/traccar/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 661cb190877..5ad5879f31b 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -324,7 +324,7 @@ class TraccarScanner: "device_traccar_id": event["deviceId"], "device_name": device_name, "type": event["type"], - "serverTime": event["serverTime"], + "serverTime": event.get("eventTime") or event.get("serverTime"), "attributes": event["attributes"], }, ) From 01c8114e93e2b230d0631124c6b999dcb1ca32c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Jul 2021 11:15:49 +0200 Subject: [PATCH 580/818] Add WLED playlist support (#53381) Co-authored-by: Anders Melchiorsen --- homeassistant/components/wled/const.py | 1 - homeassistant/components/wled/light.py | 9 -- homeassistant/components/wled/select.py | 37 +++++++- tests/components/wled/test_light.py | 3 - tests/components/wled/test_select.py | 120 ++++++++++++++++++++++++ tests/fixtures/wled/rgbw.json | 42 ++++++++- 6 files changed, 196 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 765de468350..180ef89c1b7 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -21,7 +21,6 @@ ATTR_LED_COUNT = "led_count" ATTR_MAX_POWER = "max_power" ATTR_ON = "on" ATTR_PALETTE = "palette" -ATTR_PLAYLIST = "playlist" ATTR_PRESET = "preset" ATTR_REVERSE = "reverse" ATTR_SEGMENT_ID = "segment_id" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 403aa5e7368..2081208e398 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -5,7 +5,6 @@ from functools import partial from typing import Any, Tuple, cast import voluptuous as vol -from wled import Playlist from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -30,7 +29,6 @@ from .const import ( ATTR_INTENSITY, ATTR_ON, ATTR_PALETTE, - ATTR_PLAYLIST, ATTR_PRESET, ATTR_REVERSE, ATTR_SEGMENT_ID, @@ -221,17 +219,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" - playlist: int | Playlist | None = self.coordinator.data.state.playlist - if isinstance(playlist, Playlist): - playlist = playlist.playlist_id - if playlist == -1: - playlist = None - segment = self.coordinator.data.state.segments[self._segment] return { ATTR_INTENSITY: segment.intensity, ATTR_PALETTE: segment.palette.name, - ATTR_PLAYLIST: playlist, ATTR_REVERSE: segment.reverse, ATTR_SPEED: segment.speed, } diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 373565b7ef7..f8473a8f26d 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import partial -from wled import Preset +from wled import Playlist, Preset from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -26,7 +26,7 @@ async def async_setup_entry( """Set up WLED select based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([WLEDPresetSelect(coordinator)]) + async_add_entities([WLEDPlaylistSelect(coordinator), WLEDPresetSelect(coordinator)]) update_segments = partial( async_update_segments, @@ -69,6 +69,39 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity): await self.coordinator.wled.preset(preset=option) +class WLEDPlaylistSelect(WLEDEntity, SelectEntity): + """Define a WLED Playlist select.""" + + _attr_icon = "mdi:play-speed" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED playlist.""" + super().__init__(coordinator=coordinator) + + self._attr_name = f"{coordinator.data.info.name} Playlist" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_playlist" + self._attr_options = [ + playlist.name for playlist in self.coordinator.data.playlists + ] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return len(self.coordinator.data.playlists) > 0 and super().available + + @property + def current_option(self) -> str | None: + """Return the currently selected playlist.""" + if not isinstance(self.coordinator.data.state.playlist, Playlist): + return None + return self.coordinator.data.state.playlist.name + + @wled_exception_handler + async def async_select_option(self, option: str) -> None: + """Set WLED segment to the selected playlist.""" + await self.coordinator.wled.playlist(playlist=option) + + class WLEDPaletteSelect(WLEDEntity, SelectEntity): """Defines a WLED Palette select.""" diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 14166e1956e..2d71126e0be 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -17,7 +17,6 @@ from homeassistant.components.light import ( from homeassistant.components.wled.const import ( ATTR_INTENSITY, ATTR_PALETTE, - ATTR_PLAYLIST, ATTR_PRESET, ATTR_REVERSE, ATTR_SPEED, @@ -58,7 +57,6 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" assert state.attributes.get(ATTR_INTENSITY) == 128 assert state.attributes.get(ATTR_PALETTE) == "Default" - assert state.attributes.get(ATTR_PLAYLIST) is None assert state.attributes.get(ATTR_PRESET) is None assert state.attributes.get(ATTR_REVERSE) is False assert state.attributes.get(ATTR_SPEED) == 32 @@ -77,7 +75,6 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" assert state.attributes.get(ATTR_INTENSITY) == 64 assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" - assert state.attributes.get(ATTR_PLAYLIST) is None assert state.attributes.get(ATTR_PRESET) is None assert state.attributes.get(ATTR_REVERSE) is False assert state.attributes.get(ATTR_SPEED) == 16 diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index abdef0c2ff5..dbc1bf7c970 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -358,6 +358,126 @@ async def test_preset_select_connection_error( mock_wled.preset.assert_called_with(preset="Preset 2") +async def test_playlist_unavailable_without_playlists( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test WLED playlist entity is unavailable when playlists are not available.""" + state = hass.states.get("select.wled_rgb_light_playlist") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_playlist_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the creation and values of the WLED selects.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("select.wled_rgbw_light_playlist") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:play-speed" + assert state.attributes.get(ATTR_OPTIONS) == ["Playlist 1", "Playlist 2"] + assert state.state == "Playlist 1" + + entry = entity_registry.async_get("select.wled_rgbw_light_playlist") + assert entry + assert entry.unique_id == "aabbccddee11_playlist" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", + ATTR_OPTION: "Playlist 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.playlist.call_count == 1 + mock_wled.playlist.assert_called_with(playlist="Playlist 2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_old_style_playlist_active( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test when old style playlist cycle is active.""" + # Set device playlist to 0, which meant "cycle" previously. + mock_wled.update.return_value.state.playlist = 0 + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_playlist") + assert state + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_playlist_select_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.playlist.side_effect = WLEDError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", + ATTR_OPTION: "Playlist 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_playlist") + assert state + assert state.state == "Playlist 1" + assert "Invalid response from API" in caplog.text + assert mock_wled.playlist.call_count == 1 + mock_wled.playlist.assert_called_with(playlist="Playlist 2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_playlist_select_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.playlist.side_effect = WLEDConnectionError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", + ATTR_OPTION: "Playlist 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_playlist") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.playlist.call_count == 1 + mock_wled.playlist.assert_called_with(playlist="Playlist 2") + + @pytest.mark.parametrize( "entity_id", ( diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json index d5ba9e8d00c..824612613b1 100644 --- a/tests/fixtures/wled/rgbw.json +++ b/tests/fixtures/wled/rgbw.json @@ -4,7 +4,7 @@ "bri": 140, "transition": 7, "ps": 1, - "pl": -1, + "pl": 3, "nl": { "on": false, "dur": 60, @@ -352,6 +352,46 @@ } ], "n": "Preset 2" + }, + "3": { + "playlist": { + "ps": [ + 1, + 2 + ], + "dur": [ + 30, + 30 + ], + "transition": [ + 7, + 7 + ], + "repeat": 0, + "r": false, + "end": 0 + }, + "n": "Playlist 1" + }, + "4": { + "playlist": { + "ps": [ + 1, + 2 + ], + "dur": [ + 30, + 30 + ], + "transition": [ + 7, + 7 + ], + "repeat": 0, + "r": false, + "end": 0 + }, + "n": "Playlist 2" } } } From ebfdfd172b532a2c202578e5b29da375141a5d7c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 26 Jul 2021 11:42:52 +0200 Subject: [PATCH 581/818] Add state class measurement to sensors where suitable for Synology DSM (#53468) --- .../components/synology_dsm/__init__.py | 26 +- .../components/synology_dsm/camera.py | 27 +- .../components/synology_dsm/const.py | 301 ++++++++++-------- 3 files changed, 195 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 9ec56b898ca..a9ca7b4c48d 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -26,9 +26,14 @@ from synology_dsm.exceptions import ( SynologyDSMRequestException, ) +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_MAC, CONF_PASSWORD, @@ -63,11 +68,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_VERIFY_SSL, DOMAIN, - ENTITY_CLASS, ENTITY_ENABLE, - ENTITY_ICON, - ENTITY_NAME, - ENTITY_UNIT, EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, @@ -131,7 +132,7 @@ async def async_setup_entry( # noqa: C901 for entity_key, entity_attrs in entries.items(): if ( device_id - and entity_attrs[ENTITY_NAME] == "Status" + and entity_attrs[ATTR_NAME] == "Status" and "Status" in entity_entry.unique_id and "(Smart)" not in entity_entry.unique_id ): @@ -142,7 +143,7 @@ async def async_setup_entry( # noqa: C901 entity_type = entity_key continue - if entity_attrs[ENTITY_NAME] == label: + if entity_attrs[ATTR_NAME] == label: entity_type = entity_key if entity_type is None: @@ -616,12 +617,13 @@ class SynologyDSMBaseEntity(CoordinatorEntity): self._api = api self._api_key = entity_type.split(":")[0] self.entity_type = entity_type.split(":")[-1] - self._name = f"{api.network.hostname} {entity_info[ENTITY_NAME]}" - self._class = entity_info[ENTITY_CLASS] + self._name = f"{api.network.hostname} {entity_info[ATTR_NAME]}" + self._class = entity_info[ATTR_DEVICE_CLASS] self._enable_default = entity_info[ENTITY_ENABLE] - self._icon = entity_info[ENTITY_ICON] - self._unit = entity_info[ENTITY_UNIT] + self._icon = entity_info[ATTR_ICON] + self._unit = entity_info[ATTR_UNIT_OF_MEASUREMENT] self._unique_id = f"{self._api.information.serial}_{entity_type}" + self._attr_state_class = entity_info[ATTR_STATE_CLASS] @property def unique_id(self) -> str: @@ -710,7 +712,9 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): self._device_model = disk["model"].strip() self._device_firmware = disk["firm"] self._device_type = disk["diskType"] - self._name = f"{self._api.network.hostname} {self._device_name} {entity_info[ENTITY_NAME]}" + self._name = ( + f"{self._api.network.hostname} {self._device_name} {entity_info[ATTR_NAME]}" + ) self._unique_id += f"_{self._device_id}" @property diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index a969107bf83..8341b8b121a 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -10,23 +10,21 @@ from synology_dsm.exceptions import ( ) from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity -from .const import ( - COORDINATOR_CAMERAS, - DOMAIN, - ENTITY_CLASS, - ENTITY_ENABLE, - ENTITY_ICON, - ENTITY_NAME, - ENTITY_UNIT, - SYNO_API, -) +from .const import COORDINATOR_CAMERAS, DOMAIN, ENTITY_ENABLE, SYNO_API _LOGGER = logging.getLogger(__name__) @@ -70,11 +68,12 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): api, f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera_id}", { - ENTITY_NAME: coordinator.data["cameras"][camera_id].name, + ATTR_NAME: coordinator.data["cameras"][camera_id].name, ENTITY_ENABLE: coordinator.data["cameras"][camera_id].is_enabled, - ENTITY_CLASS: None, - ENTITY_ICON: None, - ENTITY_UNIT: None, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_STATE_CLASS: None, }, coordinator, ) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index e8b919f09d5..fdbbb5678c2 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -11,7 +11,12 @@ from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation from homeassistant.components.binary_sensor import DEVICE_CLASS_SAFETY +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, DATA_TERABYTES, @@ -25,9 +30,10 @@ class EntityInfo(TypedDict): """TypedDict for EntityInfo.""" name: str - unit: str | None + unit_of_measurement: str | None icon: str | None device_class: str | None + state_class: str | None enable: bool @@ -58,11 +64,6 @@ DEFAULT_SCAN_INTERVAL = 15 # min DEFAULT_TIMEOUT = 10 # sec ENTITY_UNIT_LOAD = "load" - -ENTITY_NAME: Final = "name" -ENTITY_UNIT: Final = "unit" -ENTITY_ICON: Final = "icon" -ENTITY_CLASS: Final = "device_class" ENTITY_ENABLE: Final = "enable" # Services @@ -78,249 +79,281 @@ SERVICES = [ # Binary sensors UPGRADE_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreUpgrade.API_KEY}:update_available": { - ENTITY_NAME: "Update available", - ENTITY_UNIT: None, - ENTITY_ICON: "mdi:update", - ENTITY_CLASS: None, + ATTR_NAME: "Update available", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: "mdi:update", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, } SECURITY_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreSecurity.API_KEY}:status": { - ENTITY_NAME: "Security status", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_SAFETY, + ATTR_NAME: "Security status", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_SAFETY, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, } STORAGE_DISK_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { - ENTITY_NAME: "Exceeded Max Bad Sectors", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_SAFETY, + ATTR_NAME: "Exceeded Max Bad Sectors", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_SAFETY, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoStorage.API_KEY}:disk_below_remain_life_thr": { - ENTITY_NAME: "Below Min Remaining Life", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_SAFETY, + ATTR_NAME: "Below Min Remaining Life", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_SAFETY, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, } # Sensors UTILISATION_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { - ENTITY_NAME: "CPU Utilization (Other)", - ENTITY_UNIT: PERCENTAGE, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Utilization (Other)", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:cpu_user_load": { - ENTITY_NAME: "CPU Utilization (User)", - ENTITY_UNIT: PERCENTAGE, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Utilization (User)", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:cpu_system_load": { - ENTITY_NAME: "CPU Utilization (System)", - ENTITY_UNIT: PERCENTAGE, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Utilization (System)", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:cpu_total_load": { - ENTITY_NAME: "CPU Utilization (Total)", - ENTITY_UNIT: PERCENTAGE, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Utilization (Total)", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": { - ENTITY_NAME: "CPU Load Average (1 min)", - ENTITY_UNIT: ENTITY_UNIT_LOAD, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Load Average (1 min)", + ATTR_UNIT_OF_MEASUREMENT: ENTITY_UNIT_LOAD, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: None, }, f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": { - ENTITY_NAME: "CPU Load Average (5 min)", - ENTITY_UNIT: ENTITY_UNIT_LOAD, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Load Average (5 min)", + ATTR_UNIT_OF_MEASUREMENT: ENTITY_UNIT_LOAD, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": { - ENTITY_NAME: "CPU Load Average (15 min)", - ENTITY_UNIT: ENTITY_UNIT_LOAD, - ENTITY_ICON: "mdi:chip", - ENTITY_CLASS: None, + ATTR_NAME: "CPU Load Average (15 min)", + ATTR_UNIT_OF_MEASUREMENT: ENTITY_UNIT_LOAD, + ATTR_ICON: "mdi:chip", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoCoreUtilization.API_KEY}:memory_real_usage": { - ENTITY_NAME: "Memory Usage (Real)", - ENTITY_UNIT: PERCENTAGE, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Usage (Real)", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:memory_size": { - ENTITY_NAME: "Memory Size", - ENTITY_UNIT: DATA_MEGABYTES, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Size", + ATTR_UNIT_OF_MEASUREMENT: DATA_MEGABYTES, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:memory_cached": { - ENTITY_NAME: "Memory Cached", - ENTITY_UNIT: DATA_MEGABYTES, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Cached", + ATTR_UNIT_OF_MEASUREMENT: DATA_MEGABYTES, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:memory_available_swap": { - ENTITY_NAME: "Memory Available (Swap)", - ENTITY_UNIT: DATA_MEGABYTES, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Available (Swap)", + ATTR_UNIT_OF_MEASUREMENT: DATA_MEGABYTES, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:memory_available_real": { - ENTITY_NAME: "Memory Available (Real)", - ENTITY_UNIT: DATA_MEGABYTES, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Available (Real)", + ATTR_UNIT_OF_MEASUREMENT: DATA_MEGABYTES, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:memory_total_swap": { - ENTITY_NAME: "Memory Total (Swap)", - ENTITY_UNIT: DATA_MEGABYTES, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Total (Swap)", + ATTR_UNIT_OF_MEASUREMENT: DATA_MEGABYTES, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:memory_total_real": { - ENTITY_NAME: "Memory Total (Real)", - ENTITY_UNIT: DATA_MEGABYTES, - ENTITY_ICON: "mdi:memory", - ENTITY_CLASS: None, + ATTR_NAME: "Memory Total (Real)", + ATTR_UNIT_OF_MEASUREMENT: DATA_MEGABYTES, + ATTR_ICON: "mdi:memory", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:network_up": { - ENTITY_NAME: "Network Up", - ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, - ENTITY_ICON: "mdi:upload", - ENTITY_CLASS: None, + ATTR_NAME: "Network Up", + ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_KILOBYTES_PER_SECOND, + ATTR_ICON: "mdi:upload", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoCoreUtilization.API_KEY}:network_down": { - ENTITY_NAME: "Network Down", - ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, - ENTITY_ICON: "mdi:download", - ENTITY_CLASS: None, + ATTR_NAME: "Network Down", + ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_KILOBYTES_PER_SECOND, + ATTR_ICON: "mdi:download", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, } STORAGE_VOL_SENSORS: dict[str, EntityInfo] = { f"{SynoStorage.API_KEY}:volume_status": { - ENTITY_NAME: "Status", - ENTITY_UNIT: None, - ENTITY_ICON: "mdi:checkbox-marked-circle-outline", - ENTITY_CLASS: None, + ATTR_NAME: "Status", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: "mdi:checkbox-marked-circle-outline", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoStorage.API_KEY}:volume_size_total": { - ENTITY_NAME: "Total Size", - ENTITY_UNIT: DATA_TERABYTES, - ENTITY_ICON: "mdi:chart-pie", - ENTITY_CLASS: None, + ATTR_NAME: "Total Size", + ATTR_UNIT_OF_MEASUREMENT: DATA_TERABYTES, + ATTR_ICON: "mdi:chart-pie", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoStorage.API_KEY}:volume_size_used": { - ENTITY_NAME: "Used Space", - ENTITY_UNIT: DATA_TERABYTES, - ENTITY_ICON: "mdi:chart-pie", - ENTITY_CLASS: None, + ATTR_NAME: "Used Space", + ATTR_UNIT_OF_MEASUREMENT: DATA_TERABYTES, + ATTR_ICON: "mdi:chart-pie", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoStorage.API_KEY}:volume_percentage_used": { - ENTITY_NAME: "Volume Used", - ENTITY_UNIT: PERCENTAGE, - ENTITY_ICON: "mdi:chart-pie", - ENTITY_CLASS: None, + ATTR_NAME: "Volume Used", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-pie", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoStorage.API_KEY}:volume_disk_temp_avg": { - ENTITY_NAME: "Average Disk Temp", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_NAME: "Average Disk Temp", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoStorage.API_KEY}:volume_disk_temp_max": { - ENTITY_NAME: "Maximum Disk Temp", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_NAME: "Maximum Disk Temp", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: None, }, } STORAGE_DISK_SENSORS: dict[str, EntityInfo] = { f"{SynoStorage.API_KEY}:disk_smart_status": { - ENTITY_NAME: "Status (Smart)", - ENTITY_UNIT: None, - ENTITY_ICON: "mdi:checkbox-marked-circle-outline", - ENTITY_CLASS: None, + ATTR_NAME: "Status (Smart)", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: "mdi:checkbox-marked-circle-outline", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: None, }, f"{SynoStorage.API_KEY}:disk_status": { - ENTITY_NAME: "Status", - ENTITY_UNIT: None, - ENTITY_ICON: "mdi:checkbox-marked-circle-outline", - ENTITY_CLASS: None, + ATTR_NAME: "Status", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: "mdi:checkbox-marked-circle-outline", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, f"{SynoStorage.API_KEY}:disk_temp": { - ENTITY_NAME: "Temperature", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_NAME: "Temperature", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, } INFORMATION_SENSORS: dict[str, EntityInfo] = { f"{SynoDSMInformation.API_KEY}:temperature": { - ENTITY_NAME: "temperature", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_NAME: "temperature", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, f"{SynoDSMInformation.API_KEY}:uptime": { - ENTITY_NAME: "last boot", - ENTITY_UNIT: None, - ENTITY_ICON: None, - ENTITY_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_NAME: "last boot", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, ENTITY_ENABLE: False, + ATTR_STATE_CLASS: None, }, } # Switch SURVEILLANCE_SWITCH: dict[str, EntityInfo] = { f"{SynoSurveillanceStation.HOME_MODE_API_KEY}:home_mode": { - ENTITY_NAME: "home mode", - ENTITY_UNIT: None, - ENTITY_ICON: "mdi:home-account", - ENTITY_CLASS: None, + ATTR_NAME: "home mode", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_ICON: "mdi:home-account", + ATTR_DEVICE_CLASS: None, ENTITY_ENABLE: True, + ATTR_STATE_CLASS: None, }, } From 882c323551fd1bc554d742cca1abdcc62059d709 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 26 Jul 2021 14:21:30 +0200 Subject: [PATCH 582/818] Update pyupgrade to v2.23.0 (#53495) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa3350b36d2..1e36ae652d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.21.2 + rev: v2.23.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 9d1d068f7cd..795a4c3bcd6 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.21.2 +pyupgrade==2.23.0 yamllint==1.26.1 From ad730524c8c5a75adc13badf45f0cffd412399d1 Mon Sep 17 00:00:00 2001 From: broadcasttechie Date: Mon, 26 Jul 2021 13:32:42 +0100 Subject: [PATCH 583/818] Reduce min scan interval to 10s for InfluxDB (#53276) --- homeassistant/components/influxdb/const.py | 2 +- homeassistant/components/influxdb/sensor.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index e66a0fe10c4..77f76745ba4 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -84,7 +84,7 @@ TEST_QUERY_V1 = "SHOW DATABASES;" TEST_QUERY_V2 = "buckets()" CODE_INVALID_INPUTS = 400 -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) RE_DIGIT_TAIL = re.compile(r"^[^\.]*\d+\.?\d+[^\.]*$") RE_DECIMAL = re.compile(r"[^\d.]+") diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 299fc595f4b..c2cb5070a4c 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -1,7 +1,9 @@ """InfluxDB component which allows you to get data from an Influx database.""" from __future__ import annotations +import datetime import logging +from typing import Final import voluptuous as vol @@ -62,6 +64,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL: Final = datetime.timedelta(seconds=60) + def _merge_connection_config_into_query(conf, query): """Merge connection details into each configured query.""" From af8f5949392fb67c1f88998a1d8e424ab61c1d81 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 26 Jul 2021 14:32:16 +0100 Subject: [PATCH 584/818] Address late review of homekit_controller (#53492) Co-authored-by: Martin Hjelmare --- .../homekit_controller/config_flow.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index a19c1d0c107..cc4addfae4f 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -279,13 +279,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) try: await pairing.list_accessories_and_characteristics() - _LOGGER.debug( - "%s (%s - %s) claims to be unpaired but isn't. It's implementation of HomeKit is defective or a zeroconf relay is broadcasting stale data", - name, - model, - hkid, - ) - return self.async_abort(reason="already_paired") except AuthenticationError: _LOGGER.debug( "%s (%s - %s) is unpaired. Removing invalid pairing for this device", @@ -294,6 +287,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hkid, ) await self.hass.config_entries.async_remove(existing.entry_id) + else: + _LOGGER.debug( + "%s (%s - %s) claims to be unpaired but isn't. " + "It's implementation of HomeKit is defective " + "or a zeroconf relay is broadcasting stale data", + name, + model, + hkid, + ) + return self.async_abort(reason="already_paired") # Set unique-id and error out if it's already configured self._abort_if_unique_id_configured(updates=updated_ip_port) From 46c3495ae06a589bf76c1f6dfe9dee7975ccba79 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 26 Jul 2021 16:17:15 +0200 Subject: [PATCH 585/818] Update pylint to 2.9.5 (#53496) --- homeassistant/auth/providers/command_line.py | 4 ++-- .../components/androidtv/media_player.py | 4 ++-- homeassistant/components/co2signal/__init__.py | 2 +- homeassistant/components/homekit/img_util.py | 2 ++ homeassistant/components/plugwise/config_flow.py | 1 - .../components/shell_command/__init__.py | 6 ++---- homeassistant/core.py | 4 ++-- homeassistant/helpers/script.py | 2 +- requirements_test.txt | 2 +- tests/components/shell_command/test_init.py | 15 +++------------ 10 files changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 65d553d4eb2..f462ad4be9d 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -1,7 +1,7 @@ """Auth provider that validates credentials via an external command.""" from __future__ import annotations -import asyncio.subprocess +import asyncio import collections from collections.abc import Mapping import logging @@ -64,7 +64,7 @@ class CommandLineAuthProvider(AuthProvider): """Validate a username and password.""" env = {"username": username, "password": password} try: - process = await asyncio.subprocess.create_subprocess_exec( # pylint: disable=no-member + process = await asyncio.create_subprocess_exec( self.config[CONF_COMMAND], *self.config[CONF_ARGS], env=env, diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 7b901e7ab37..98d1ac0ae18 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -376,13 +376,13 @@ def adb_decorator(override_available=False): err, ) await self.aftv.adb_close() - self._attr_available = False # pylint: disable=protected-access + self._attr_available = False return None except Exception: # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again, then raise the exception. await self.aftv.adb_close() - self._attr_available = False # pylint: disable=protected-access + self._attr_available = False raise return _adb_exception_catcher diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index 734eb9f1ae0..26f41ac2e67 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -134,7 +134,7 @@ def get_data(hass: HomeAssistant, config: dict) -> CO2SignalResponse: _LOGGER.exception("Unexpected exception") raise UnknownError from err - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.exception("Unexpected exception") raise UnknownError from err diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py index 860d798f113..7d7a45081a6 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/homekit/img_util.py @@ -53,6 +53,8 @@ class TurboJPEGSingleton: def __init__(self): """Try to create TurboJPEG only once.""" + # pylint: disable=unused-private-member + # https://github.com/PyCQA/pylint/issues/4681 try: # TurboJPEG checks for libturbojpeg # when its created, but it imports diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 5f32aca13a0..450388b6f42 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -117,7 +117,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_HOST: self.discovery_info[CONF_HOST], CONF_NAME: _name, diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 48744b4fea2..a0dfd3388b4 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -60,8 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if rendered_args == args: # No template used. default behavior - # pylint: disable=no-member - create_process = asyncio.subprocess.create_subprocess_shell( + create_process = asyncio.create_subprocess_shell( cmd, stdin=None, stdout=asyncio.subprocess.PIPE, @@ -72,8 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # (which uses shell=False) for security shlexed_cmd = [prog] + shlex.split(rendered_args) - # pylint: disable=no-member - create_process = asyncio.subprocess.create_subprocess_exec( + create_process = asyncio.create_subprocess_exec( *shlexed_cmd, stdin=None, stdout=asyncio.subprocess.PIPE, diff --git a/homeassistant/core.py b/homeassistant/core.py index 0c2658952ce..aba4483f192 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -382,14 +382,14 @@ class HomeAssistant: self.loop.call_soon_threadsafe(self.async_create_task, target) @callback - def async_create_task(self, target: Awaitable) -> asyncio.tasks.Task: + def async_create_task(self, target: Awaitable) -> asyncio.Task: """Create a task from within the eventloop. This method must be run in the event loop. target: target to call. """ - task: asyncio.tasks.Task = self.loop.create_task(target) + task: asyncio.Task = self.loop.create_task(target) if self._track_task: self._pending_tasks.append(task) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index addd2ae046d..e5e8ef4fd52 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -504,7 +504,7 @@ class _ScriptRun: task.cancel() unsub() - async def _async_run_long_action(self, long_task: asyncio.tasks.Task) -> None: + async def _async_run_long_action(self, long_task: asyncio.Task) -> None: """Run a long task while monitoring for stop request.""" async def async_cancel_long_task() -> None: diff --git a/requirements_test.txt b/requirements_test.txt index 8b6ab238037..aceec3229a9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.902 pre-commit==2.13.0 -pylint==2.9.3 +pylint==2.9.5 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index b86fe12516d..196e47351bc 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -61,10 +61,7 @@ async def test_config_not_valid_service_names(hass): ) -@patch( - "homeassistant.components.shell_command.asyncio.subprocess" - ".create_subprocess_shell" -) +@patch("homeassistant.components.shell_command.asyncio.create_subprocess_shell") async def test_template_render_no_template(mock_call, hass): """Ensure shell_commands without templates get rendered properly.""" mock_call.return_value = mock_process_creator(error=False) @@ -84,10 +81,7 @@ async def test_template_render_no_template(mock_call, hass): assert cmd == "ls /bin" -@patch( - "homeassistant.components.shell_command.asyncio.subprocess" - ".create_subprocess_exec" -) +@patch("homeassistant.components.shell_command.asyncio.create_subprocess_exec") async def test_template_render(mock_call, hass): """Ensure shell_commands with templates get rendered properly.""" hass.states.async_set("sensor.test_state", "Works") @@ -111,10 +105,7 @@ async def test_template_render(mock_call, hass): assert ("ls", "/bin", "Works") == cmd -@patch( - "homeassistant.components.shell_command.asyncio.subprocess" - ".create_subprocess_shell" -) +@patch("homeassistant.components.shell_command.asyncio.create_subprocess_shell") @patch("homeassistant.components.shell_command._LOGGER.error") async def test_subprocess_error(mock_error, mock_call, hass): """Test subprocess that returns an error.""" From 2025afe14b18d35b186003057edbd44eec8bf47a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 Jul 2021 16:43:05 +0200 Subject: [PATCH 586/818] Add MAC to SamsungTV when missing (#53479) * Add MAC when missing * Fix I/O * Add test for missing MAC address --- .../components/samsungtv/__init__.py | 14 ++++-- .../components/samsungtv/config_flow.py | 3 ++ .../components/samsungtv/manifest.json | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + .../components/samsungtv/test_config_flow.py | 49 +++++++++++++++++++ 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 09b513c3830..773c340d7b9 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,6 +1,8 @@ """The Samsung TV integration.""" +from functools import partial import socket +import getmac import voluptuous as vol from homeassistant import config_entries @@ -140,13 +142,19 @@ async def _async_create_bridge_with_updated_data(hass, entry): bridge = _async_get_device_bridge({**entry.data, **updated_data}) - if not entry.data.get(CONF_MAC) and bridge.method == METHOD_WEBSOCKET: + mac = entry.data.get(CONF_MAC) + if not mac and bridge.method == METHOD_WEBSOCKET: if info: mac = mac_from_device_info(info) else: mac = await hass.async_add_executor_job(bridge.mac_from_device) - if mac: - updated_data[CONF_MAC] = mac + + if not mac: + mac = await hass.async_add_executor_job( + partial(getmac.get_mac_address, ip=host) + ) + if mac: + updated_data[CONF_MAC] = mac if updated_data: data = {**entry.data, **updated_data} diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 392beda6ac5..da13d0fe70c 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -2,6 +2,7 @@ import socket from urllib.parse import urlparse +import getmac import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -154,6 +155,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if mac := mac_from_device_info(info): self._mac = mac + elif mac := getmac.get_mac_address(ip=self._host): + self._mac = mac self._device_info = info return True diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 133baccf4fb..36481b43756 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -3,6 +3,7 @@ "name": "Samsung Smart TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ + "getmac==0.8.2", "samsungctl[websocket]==0.7.1", "samsungtvws==1.6.0", "wakeonlan==2.0.1" diff --git a/requirements_all.txt b/requirements_all.txt index 523bca15f47..47bc55074be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,6 +679,7 @@ georss_qld_bushfire_alert_client==0.5 # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker +# homeassistant.components.samsungtv getmac==0.8.2 # homeassistant.components.gios diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2fd0df2fe0..f05af08862e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -388,6 +388,7 @@ georss_qld_bushfire_alert_client==0.5 # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker +# homeassistant.components.samsungtv getmac==0.8.2 # homeassistant.components.gios diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4fe8ddc2b5e..64d0c95c084 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -797,6 +797,55 @@ async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" +async def test_websocket_no_mac(hass: HomeAssistant, remote: Mock, remotews: Mock): + """Test for send key with autodetection of protocol.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews, patch( + "getmac.get_mac_address", return_value="gg:hh:ii:ll:mm:nn" + ): + enter = Mock() + type(enter).token = PropertyMock(return_value="123456789") + remote = Mock() + remote.__enter__ = Mock(return_value=enter) + remote.__exit__ = Mock(return_value=False) + remote.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "networkType": "lan", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + }, + } + remotews.return_value = remote + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_METHOD] == "websocket" + assert result["data"][CONF_TOKEN] == "123456789" + assert result["data"][CONF_MAC] == "gg:hh:ii:ll:mm:nn" + assert remotews.call_count == 2 + assert remotews.call_args_list == [ + call(**AUTODETECT_WEBSOCKET_SSL), + call(**DEVICEINFO_WEBSOCKET_SSL), + ] + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_MAC] == "gg:hh:ii:ll:mm:nn" + + async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch( From 37d6824ed41e40940316a880fa7279279f9ec34f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 Jul 2021 16:44:58 +0200 Subject: [PATCH 587/818] Increase pool max size for urllib3 in Fritz integration (#53461) --- homeassistant/components/fritz/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 707292ceab2..2a9a5e5cd2e 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -108,6 +108,7 @@ class FritzBoxTools: user=self.username, password=self.password, timeout=60.0, + pool_maxsize=30, ) if not self.connection: From d58a02a6479b0fdbf5abfae3fc516ccf7ae12ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 26 Jul 2021 17:42:19 +0200 Subject: [PATCH 588/818] Broadlink, remove attr_current_power_w and add sensor (#53342) --- homeassistant/components/broadlink/const.py | 11 ++++++++++- homeassistant/components/broadlink/sensor.py | 9 ++++++++- homeassistant/components/broadlink/switch.py | 2 -- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index fd060d23b35..ed56e42342d 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -7,7 +7,16 @@ DOMAIN = "broadlink" DOMAINS_AND_TYPES = { REMOTE_DOMAIN: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, - SENSOR_DOMAIN: {"A1", "RM4MINI", "RM4PRO", "RMPRO"}, + SENSOR_DOMAIN: { + "A1", + "RM4MINI", + "RM4PRO", + "RMPRO", + "SP2S", + "SP3S", + "SP4", + "SP4B", + }, SWITCH_DOMAIN: { "BG1", "MP1", diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 044486a4a67..2e5a82e1217 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -6,12 +6,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import CONF_HOST, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import CONF_HOST, PERCENTAGE, POWER_WATT, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -37,6 +38,12 @@ SENSOR_TYPES = { ), "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE, None), "noise": ("Noise", None, None, None), + "power": ( + "Current power", + POWER_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 9f434102b17..3333c370824 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -232,14 +232,12 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): """Initialize the switch.""" super().__init__(device, *args, **kwargs) self._attr_is_on = self._coordinator.data["pwr"] - self._attr_current_power_w = self._coordinator.data.get("power") @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: self._attr_is_on = self._coordinator.data["pwr"] - self._attr_current_power_w = self._coordinator.data.get("power") self.async_write_ha_state() From 9a000a1183c0bb498fd0b7c0886d63c8e2783f51 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 26 Jul 2021 16:46:36 +0100 Subject: [PATCH 589/818] Support controlling Flowerbud spray level via homekit_controller (#53493) --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/number.py | 100 ++++ .../test_vocolinc_flowerbud.py | 70 +++ .../homekit_controller/test_number.py | 87 ++++ .../vocolinc_flowerbud.json | 467 ++++++++++++++++++ 5 files changed, 725 insertions(+) create mode 100644 homeassistant/components/homekit_controller/number.py create mode 100644 tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py create mode 100644 tests/components/homekit_controller/test_number.py create mode 100644 tests/fixtures/homekit_controller/vocolinc_flowerbud.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 5ac777a3969..321113cf8df 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -47,5 +47,6 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: "sensor", + CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: "number", CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): "sensor", } diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py new file mode 100644 index 00000000000..2e0193fa080 --- /dev/null +++ b/homeassistant/components/homekit_controller/number.py @@ -0,0 +1,100 @@ +""" +Support for Homekit number ranges. + +These are mostly used where a HomeKit accessory exposes additional non-standard +characteristics that don't map to a Home Assistant feature. +""" +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes + +from homeassistant.components.number import NumberEntity +from homeassistant.core import callback + +from . import KNOWN_DEVICES, CharacteristicEntity + +NUMBER_ENTITIES = { + CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: { + "name": "Spray Quantity", + "icon": "mdi:water", + } +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit numbers.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_characteristic(char: Characteristic): + kwargs = NUMBER_ENTITIES.get(char.type) + if not kwargs: + return False + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities([HomeKitNumber(conn, info, char, **kwargs)], True) + return True + + conn.add_char_factory(async_add_characteristic) + + +class HomeKitNumber(CharacteristicEntity, NumberEntity): + """Representation of a Number control on a homekit accessory.""" + + def __init__( + self, + conn, + info, + char, + device_class=None, + icon=None, + name=None, + **kwargs, + ): + """Initialise a HomeKit number control.""" + self._device_class = device_class + self._icon = icon + self._name = name + self._char = char + + super().__init__(conn, info) + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [self._char.type] + + @property + def device_class(self): + """Return type of sensor.""" + return self._device_class + + @property + def icon(self): + """Return the sensor icon.""" + return self._icon + + @property + def min_value(self) -> float: + """Return the minimum value.""" + return self._char.minValue + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._char.maxValue + + @property + def step(self) -> float: + """Return the increment/decrement step.""" + return self._char.minStep + + @property + def value(self) -> float: + """Return the current characteristic value.""" + return self._char.value + + async def async_set_value(self, value: float): + """Set the characteristic to this value.""" + await self.async_put_characteristics( + { + self._char.type: value, + } + ) diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py new file mode 100644 index 00000000000..ee4713b012b --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -0,0 +1,70 @@ +"""Make sure that Vocolinc Flowerbud is enumerated properly.""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_vocolinc_flowerbud_setup(hass): + """Test that a Vocolinc Flowerbud can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "vocolinc_flowerbud.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Check that the switch entity is handled correctly + + entry = entity_registry.async_get("number.vocolinc_flowerbud_0d324b") + assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:30" + + helper = Helper( + hass, "number.vocolinc_flowerbud_0d324b", pairing, accessories[0], config_entry + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "VOCOlinc-Flowerbud-0d324b" + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "VOCOlinc" + assert device.name == "VOCOlinc-Flowerbud-0d324b" + assert device.model == "Flowerbud" + assert device.sw_version == "3.121.2" + assert device.via_device_id is None + + # Assert the humidifier is detected + entry = entity_registry.async_get("humidifier.vocolinc_flowerbud_0d324b") + assert entry.unique_id == "homekit-AM01121849000327-30" + + helper = Helper( + hass, + "humidifier.vocolinc_flowerbud_0d324b", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "VOCOlinc-Flowerbud-0d324b" + + # The sensor and switch should be part of the same device + assert entry.device_id == device.id + + # Assert the light is detected + entry = entity_registry.async_get("light.vocolinc_flowerbud_0d324b") + assert entry.unique_id == "homekit-AM01121849000327-9" + + helper = Helper( + hass, + "light.vocolinc_flowerbud_0d324b", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "VOCOlinc-Flowerbud-0d324b" + + # The sensor and switch should be part of the same device + assert entry.device_id == device.id diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py new file mode 100644 index 00000000000..490b69b1a80 --- /dev/null +++ b/tests/components/homekit_controller/test_number.py @@ -0,0 +1,87 @@ +"""Basic checks for HomeKit sensor.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from tests.components.homekit_controller.common import Helper, setup_test_component + + +def create_switch_with_spray_level(accessory): + """Define battery level characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + spray_level = service.add_char( + CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL + ) + + spray_level.value = 1 + spray_level.minStep = 1 + spray_level.minValue = 1 + spray_level.maxValue = 5 + spray_level.format = "float" + + cur_state = service.add_char(CharacteristicsTypes.ON) + cur_state.value = True + + return service + + +async def test_read_number(hass, utcnow): + """Test a switch service that has a sensor characteristic is correctly handled.""" + helper = await setup_test_component(hass, create_switch_with_spray_level) + outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. + energy_helper = Helper( + hass, + "number.testdevice", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + spray_level = outlet[CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL] + + state = await energy_helper.poll_and_get_state() + assert state.state == "1" + assert state.attributes["step"] == 1 + assert state.attributes["min"] == 1 + assert state.attributes["max"] == 5 + + spray_level.value = 5 + state = await energy_helper.poll_and_get_state() + assert state.state == "5" + + +async def test_write_number(hass, utcnow): + """Test a switch service that has a sensor characteristic is correctly handled.""" + helper = await setup_test_component(hass, create_switch_with_spray_level) + outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. + energy_helper = Helper( + hass, + "number.testdevice", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + spray_level = outlet[CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL] + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice", "value": 5}, + blocking=True, + ) + assert spray_level.value == 5 + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice", "value": 3}, + blocking=True, + ) + assert spray_level.value == 3 diff --git a/tests/fixtures/homekit_controller/vocolinc_flowerbud.json b/tests/fixtures/homekit_controller/vocolinc_flowerbud.json new file mode 100644 index 00000000000..012c03471f3 --- /dev/null +++ b/tests/fixtures/homekit_controller/vocolinc_flowerbud.json @@ -0,0 +1,467 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 2, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "VOCOlinc" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Flowerbud" + }, + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "VOCOlinc-Flowerbud-0d324b" + }, + { + "format": "string", + "iid": 6, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "AM01121849000327" + }, + { + "description": "", + "format": "string", + "iid": 7, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "3.121.2" + }, + { + "format": "string", + "iid": 8, + "perms": [ + "pr" + ], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "0.1" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "rssi_report_switch", + "format": "bool", + "iid": 81, + "perms": [ + "pr", + "pw" + ], + "type": "D9959C8A-809A-4F75-92D7-71F630AC2925", + "value": 0 + }, + { + "description": "rssi_report_value", + "format": "uint8", + "iid": 82, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "8137182C-6904-4FB9-ADCC-61CECA85CE48", + "value": 0 + } + ], + "iid": 80, + "stype": "Unknown Service: C635EF5C-5BBC-4F96-B7DA-6669069A4B32", + "type": "C635EF5C-5BBC-4F96-B7DA-6669069A4B32" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 31, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "FLOWERBUD" + }, + { + "format": "uint8", + "iid": 32, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000B0-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "float", + "iid": 33, + "maxValue": 100.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": [ + "pr", + "ev" + ], + "type": "00000010-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 45.0 + }, + { + "format": "uint8", + "iid": 34, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "000000B3-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "uint8", + "iid": 35, + "maxValue": 1, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000B4-0000-1000-8000-0026BB765291", + "value": 1 + }, + { + "format": "uint8", + "iid": 36, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "36158AC8-5191-4AE2-9EF5-1D6722E88E3D", + "value": 1 + }, + { + "description": "spray quantity", + "format": "uint8", + "iid": 38, + "maxValue": 5, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "69D52519-0A4E-4898-8335-4739F9116D0A", + "value": 5 + }, + { + "format": "float", + "iid": 39, + "maxValue": 100.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CA-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100.0 + }, + { + "description": "humidifier_timer_setting", + "format": "data", + "iid": 40, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "F84B3138-E44F-49B9-AA91-9E1736C247C0", + "value": "AA==" + }, + { + "description": "humidifier_countdown", + "format": "data", + "iid": 41, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "43CE176B-2933-4034-98A7-AD215BEEBF2F", + "value": "AA==" + } + ], + "iid": 30, + "stype": "humidifier-dehumidifier", + "type": "000000BD-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 10, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Mood Light" + }, + { + "format": "bool", + "iid": 11, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "int", + "iid": 12, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 50 + }, + { + "format": "float", + "iid": 13, + "maxValue": 360.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000013-0000-1000-8000-0026BB765291", + "unit": "arcdegrees", + "value": 120.0 + }, + { + "format": "float", + "iid": 14, + "maxValue": 100.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "0000002F-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100.0 + }, + { + "description": "lb_timer_setting", + "format": "data", + "iid": 63, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "A30DFE91-271A-42A5-88BA-00E3FF5488AD", + "value": "AA==" + }, + { + "description": "light effect mode", + "format": "uint8", + "iid": 64, + "maxValue": 31, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "146889FC-7C42-429B-93AB-E80F79759E90", + "value": 0 + }, + { + "description": "light effect flag", + "format": "uint32", + "iid": 73, + "perms": [ + "pr" + ], + "type": "9D4B479D-9EFB-4739-98F3-B33E6543BF7B", + "value": 7 + }, + { + "description": "flashing mode", + "format": "data", + "iid": 65, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2C42B339-6EC9-4ED5-8DBF-FFCCC721B144", + "value": "AA==" + }, + { + "description": "smoothing mode", + "format": "data", + "iid": 66, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "A3663C89-DC18-42EF-8297-910A4C0C9B61", + "value": "AA==" + }, + { + "description": "breathing mode", + "format": "data", + "iid": 67, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "6533B15C-AECB-455F-8896-20B125390F61", + "value": "AA==" + } + ], + "iid": 9, + "stype": "lightbulb", + "type": "00000043-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "time_zone", + "format": "int", + "iid": 50, + "maxValue": 1400, + "minStep": 1, + "minValue": -1200, + "perms": [ + "pr", + "pw" + ], + "type": "38396B8E-161B-4A77-AF3F-C4DAC0BE9B74", + "value": 0 + }, + { + "description": "hour_date_time", + "format": "int", + "iid": 51, + "perms": [ + "pr", + "pw" + ], + "type": "71216CD3-209E-40CC-BEA0-71A2A9458E13", + "value": 0 + } + ], + "iid": 48, + "stype": "Unknown Service: 961EBB65-A1E3-4F34-BD31-86552706FE40", + "type": "961EBB65-A1E3-4F34-BD31-86552706FE40" + }, + { + "characteristics": [ + { + "description": "fm_upgrade_status", + "format": "int", + "iid": 21, + "perms": [ + "pr", + "ev" + ], + "type": "49DDDE07-C3FA-499E-8055-58E154E04F34", + "value": 0 + }, + { + "description": "fm_upgrade_url", + "format": "string", + "iid": 22, + "maxLen": 256, + "perms": [ + "pw" + ], + "type": "4C203E30-EB25-466D-9980-C6C2E14BF6AA" + } + ], + "hidden": true, + "iid": 20, + "stype": "Unknown Service: 3138B537-E830-4F52-90A7-D6FDB000BF97", + "type": "3138B537-E830-4F52-90A7-D6FDB000BF97" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 24, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 23, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + } + ] + } +] From a6331d85ed190101b63338dd4c4f4ad9cf438644 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 26 Jul 2021 08:50:22 -0700 Subject: [PATCH 590/818] Support energy/power sensors in the WeMo component (#53419) --- homeassistant/components/wemo/__init__.py | 64 +++---- homeassistant/components/wemo/sensor.py | 132 +++++++++++++++ tests/components/wemo/conftest.py | 2 +- tests/components/wemo/entity_test_helpers.py | 19 ++- tests/components/wemo/test_sensor.py | 165 +++++++++++++++++++ 5 files changed, 344 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/wemo/sensor.py create mode 100644 tests/components/wemo/test_sensor.py diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 2e803bc07bf..aa7b5ff05c1 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP @@ -28,17 +29,17 @@ MAX_CONCURRENCY = 3 # Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { - "Bridge": LIGHT_DOMAIN, - "CoffeeMaker": SWITCH_DOMAIN, - "Dimmer": LIGHT_DOMAIN, - "Humidifier": FAN_DOMAIN, - "Insight": SWITCH_DOMAIN, - "LightSwitch": SWITCH_DOMAIN, - "Maker": SWITCH_DOMAIN, - "Motion": BINARY_SENSOR_DOMAIN, - "OutdoorPlug": SWITCH_DOMAIN, - "Sensor": BINARY_SENSOR_DOMAIN, - "Socket": SWITCH_DOMAIN, + "Bridge": [LIGHT_DOMAIN], + "CoffeeMaker": [SWITCH_DOMAIN], + "Dimmer": [LIGHT_DOMAIN], + "Humidifier": [FAN_DOMAIN], + "Insight": [SENSOR_DOMAIN, SWITCH_DOMAIN], + "LightSwitch": [SWITCH_DOMAIN], + "Maker": [SWITCH_DOMAIN], + "Motion": [BINARY_SENSOR_DOMAIN], + "OutdoorPlug": [SWITCH_DOMAIN], + "Sensor": [BINARY_SENSOR_DOMAIN], + "Socket": [SWITCH_DOMAIN], } _LOGGER = logging.getLogger(__name__) @@ -151,32 +152,31 @@ class WemoDispatcher: if wemo.serialnumber in self._added_serial_numbers: return - component = WEMO_MODEL_DISPATCH.get(wemo.model_name, SWITCH_DOMAIN) device = await async_register_device(hass, self._config_entry, wemo) + for component in WEMO_MODEL_DISPATCH.get(wemo.model_name, [SWITCH_DOMAIN]): + # Three cases: + # - First time we see component, we need to load it and initialize the backlog + # - Component is being loaded, add to backlog + # - Component is loaded, backlog is gone, dispatch discovery - # Three cases: - # - First time we see component, we need to load it and initialize the backlog - # - Component is being loaded, add to backlog - # - Component is loaded, backlog is gone, dispatch discovery - - if component not in self._loaded_components: - hass.data[DOMAIN]["pending"][component] = [device] - self._loaded_components.add(component) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - self._config_entry, component + if component not in self._loaded_components: + hass.data[DOMAIN]["pending"][component] = [device] + self._loaded_components.add(component) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + self._config_entry, component + ) ) - ) - elif component in hass.data[DOMAIN]["pending"]: - hass.data[DOMAIN]["pending"][component].append(device) + elif component in hass.data[DOMAIN]["pending"]: + hass.data[DOMAIN]["pending"][component].append(device) - else: - async_dispatcher_send( - hass, - f"{DOMAIN}.{component}", - device, - ) + else: + async_dispatcher_send( + hass, + f"{DOMAIN}.{component}", + device, + ) self._added_serial_numbers.add(wemo.serialnumber) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py new file mode 100644 index 00000000000..426cb80adce --- /dev/null +++ b/homeassistant/components/wemo/sensor.py @@ -0,0 +1,132 @@ +"""Support for power sensors in WeMo Insight devices.""" +import asyncio +from datetime import datetime, timedelta +from typing import Callable + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + STATE_UNAVAILABLE, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import StateType +from homeassistant.util import Throttle, convert, dt + +from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoSubscriptionEntity +from .wemo_device import DeviceWrapper + +SCAN_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo sensors.""" + + async def _discovered_wemo(device: DeviceWrapper): + """Handle a discovered Wemo device.""" + + @Throttle(SCAN_INTERVAL) + def update_insight_params(): + device.wemo.update_insight_params() + + async_add_entities( + [ + InsightCurrentPower(device, update_insight_params), + InsightTodayEnergy(device, update_insight_params), + ] + ) + + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo) + + await asyncio.gather( + *( + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("sensor") + ) + ) + + +class InsightSensor(WemoSubscriptionEntity, SensorEntity): + """Common base for WeMo Insight power sensors.""" + + def __init__( + self, + device: DeviceWrapper, + update_insight_params: Callable, + name_suffix: str, + device_class: str, + unit_of_measurement: str, + ) -> None: + """Initialize the WeMo Insight power sensor.""" + super().__init__(device) + self._update_insight_params = update_insight_params + self._name_suffix = name_suffix + self._attr_device_class = device_class + self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_unit_of_measurement = unit_of_measurement + + @property + def name(self) -> str: + """Return the name of the entity if any.""" + return f"{self.wemo.name} {self._name_suffix}" + + @property + def unique_id(self) -> str: + """Return the id of this entity.""" + return f"{self.wemo.serialnumber}_{self._name_suffix}" + + def _update(self, force_update=True) -> None: + with self._wemo_exception_handler("update status"): + if force_update or not self.wemo.insight_params: + self._update_insight_params() + + +class InsightCurrentPower(InsightSensor): + """Current instantaineous power consumption.""" + + def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None: + """Initialize the WeMo Insight power sensor.""" + super().__init__( + device, + update_insight_params, + "Current Power", + DEVICE_CLASS_POWER, + POWER_WATT, + ) + + @property + def state(self) -> StateType: + """Return the current power consumption.""" + if "currentpower" not in self.wemo.insight_params: + return STATE_UNAVAILABLE + return convert(self.wemo.insight_params["currentpower"], float, 0.0) / 1000.0 + + +class InsightTodayEnergy(InsightSensor): + """Energy used today.""" + + def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None: + """Initialize the WeMo Insight power sensor.""" + super().__init__( + device, + update_insight_params, + "Today Energy", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ) + + @property + def last_reset(self) -> datetime: + """Return the time when the sensor was initialized.""" + return dt.start_of_local_day() + + @property + def state(self) -> StateType: + """Return the current energy use today.""" + if "todaymw" not in self.wemo.insight_params: + return STATE_UNAVAILABLE + miliwatts = convert(self.wemo.insight_params["todaymw"], float, 0.0) + return round(miliwatts / (1000.0 * 1000.0 * 60), 2) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index ba1995e8c83..7766fe512cc 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -19,7 +19,7 @@ MOCK_SERIAL_NUMBER = "WemoSerialNumber" @pytest.fixture(name="pywemo_model") def pywemo_model_fixture(): """Fixture containing a pywemo class name used by pywemo_device_fixture.""" - return "Insight" + return "LightSwitch" @pytest.fixture(name="pywemo_registry") diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 9289d4a0171..3d1a73941e6 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -7,6 +7,7 @@ import threading from unittest.mock import patch import async_timeout +import pywemo from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.homeassistant import ( @@ -139,10 +140,14 @@ async def test_async_update_locked_multiple_callbacks( async def test_async_locked_update_with_exception( - hass, wemo_entity, pywemo_device, update_polling_method=None + hass, + wemo_entity, + pywemo_device, + update_polling_method=None, + expected_state=STATE_OFF, ): """Test that the entity becomes unavailable when communication is lost.""" - assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + assert hass.states.get(wemo_entity.entity_id).state == expected_state await async_setup_component(hass, HA_DOMAIN, {}) update_polling_method = update_polling_method or pywemo_device.get_state update_polling_method.side_effect = ActionException @@ -157,9 +162,11 @@ async def test_async_locked_update_with_exception( assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE -async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_device): +async def test_async_update_with_timeout_and_recovery( + hass, wemo_entity, pywemo_device, expected_state=STATE_OFF +): """Test that the entity becomes unavailable after a timeout, and that it recovers.""" - assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + assert hass.states.get(wemo_entity.entity_id).state == expected_state await async_setup_component(hass, HA_DOMAIN, {}) event = threading.Event() @@ -170,6 +177,8 @@ async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_ if hasattr(pywemo_device, "bridge_update"): pywemo_device.bridge_update.side_effect = get_state + elif isinstance(pywemo_device, pywemo.Insight): + pywemo_device.update_insight_params.side_effect = get_state else: pywemo_device.get_state.side_effect = get_state timeout = async_timeout.timeout(0) @@ -187,4 +196,4 @@ async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_ # Check that the entity recovers and is available after the update succeeds. event.set() await hass.async_block_till_done() - assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + assert hass.states.get(wemo_entity.entity_id).state == expected_state diff --git a/tests/components/wemo/test_sensor.py b/tests/components/wemo/test_sensor.py new file mode 100644 index 00000000000..3b8786131a7 --- /dev/null +++ b/tests/components/wemo/test_sensor.py @@ -0,0 +1,165 @@ +"""Tests for the Wemo sensor entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import entity_test_helpers +from .conftest import MOCK_HOST, MOCK_PORT + + +@pytest.fixture +def pywemo_model(): + """Pywemo LightSwitch models use the switch platform.""" + return "Insight" + + +@pytest.fixture(name="pywemo_device") +def pywemo_device_fixture(pywemo_device): + """Fixture for WeMoDevice instances.""" + pywemo_device.insight_params = { + "currentpower": 1.0, + "todaymw": 200000000.0, + "state": 0, + "onfor": 0, + "ontoday": 0, + "ontotal": 0, + "powerthreshold": 0, + } + yield pywemo_device + + +class InsightTestTemplate: + """Base class for testing WeMo Insight Sensors.""" + + ENTITY_ID_SUFFIX: str + EXPECTED_STATE_VALUE: str + INSIGHT_PARAM_NAME: str + + @pytest.fixture(name="wemo_entity") + @classmethod + async def async_wemo_entity_fixture(cls, hass, pywemo_device): + """Fixture for a Wemo entity in hass.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [f"{MOCK_HOST}:{MOCK_PORT}"], + }, + }, + ) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + correct_entity = None + to_remove = [] + for entry in entity_registry.entities.values(): + if entry.entity_id.endswith(cls.ENTITY_ID_SUFFIX): + correct_entity = entry + else: + to_remove.append(entry.entity_id) + + for removal in to_remove: + entity_registry.async_remove(removal) + assert len(entity_registry.entities) == 1 + return correct_entity + + # Tests that are in common among wemo platforms. These test methods will be run + # in the scope of this test module. They will run using the pywemo_model from + # this test module (Insight). + async def test_async_update_locked_multiple_updates( + self, hass, pywemo_registry, wemo_entity, pywemo_device + ): + """Test that two hass async_update state updates do not proceed at the same time.""" + pywemo_device.subscription_update.return_value = False + await entity_test_helpers.test_async_update_locked_multiple_updates( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.update_insight_params, + ) + + async def test_async_update_locked_multiple_callbacks( + self, hass, pywemo_registry, wemo_entity, pywemo_device + ): + """Test that two device callback state updates do not proceed at the same time.""" + pywemo_device.subscription_update.return_value = False + await entity_test_helpers.test_async_update_locked_multiple_callbacks( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.update_insight_params, + ) + + async def test_async_update_locked_callback_and_update( + self, hass, pywemo_registry, wemo_entity, pywemo_device + ): + """Test that a callback and a state update request can't both happen at the same time.""" + pywemo_device.subscription_update.return_value = False + await entity_test_helpers.test_async_update_locked_callback_and_update( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.update_insight_params, + ) + + async def test_async_locked_update_with_exception( + self, hass, wemo_entity, pywemo_device + ): + """Test that the entity becomes unavailable when communication is lost.""" + await entity_test_helpers.test_async_locked_update_with_exception( + hass, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.update_insight_params, + expected_state=self.EXPECTED_STATE_VALUE, + ) + + async def test_async_update_with_timeout_and_recovery( + self, hass, wemo_entity, pywemo_device + ): + """Test that the entity becomes unavailable after a timeout, and that it recovers.""" + await entity_test_helpers.test_async_update_with_timeout_and_recovery( + hass, wemo_entity, pywemo_device, expected_state=self.EXPECTED_STATE_VALUE + ) + + async def test_state_unavailable(self, hass, wemo_entity, pywemo_device): + """Test that there is no failure if the insight_params is not populated.""" + del pywemo_device.insight_params[self.INSIGHT_PARAM_NAME] + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + + +class TestInsightCurrentPower(InsightTestTemplate): + """Test the InsightCurrentPower class.""" + + ENTITY_ID_SUFFIX = "_current_power" + EXPECTED_STATE_VALUE = "0.001" + INSIGHT_PARAM_NAME = "currentpower" + + +class TestInsightTodayEnergy(InsightTestTemplate): + """Test the InsightTodayEnergy class.""" + + ENTITY_ID_SUFFIX = "_today_energy" + EXPECTED_STATE_VALUE = "3.33" + INSIGHT_PARAM_NAME = "todaymw" From 4b189bd8c55a9cd614f6191b9030ca26c06446f4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 26 Jul 2021 11:59:16 -0400 Subject: [PATCH 591/818] Add zwave_js WS API commands to get statistics (#53393) * Add zwave_js WS API commands to get statistics * update function name * move nested functions to top level functions --- homeassistant/components/zwave_js/api.py | 156 ++++++++++++++++-- tests/components/zwave_js/test_api.py | 192 +++++++++++++++++++++++ 2 files changed, 336 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 379376bb98d..a55ae47b935 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -19,13 +19,14 @@ from zwave_js_server.exceptions import ( SetValueFailed, ) from zwave_js_server.firmware import begin_firmware_update +from zwave_js_server.model.controller import ControllerStatistics from zwave_js_server.model.firmware import ( FirmwareUpdateFinished, FirmwareUpdateProgress, ) from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage -from zwave_js_server.model.node import Node +from zwave_js_server.model.node import Node, NodeStatistics from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api @@ -200,6 +201,10 @@ def async_register_api(hass: HomeAssistant) -> None: ) websocket_api.async_register_command(hass, websocket_check_for_config_updates) websocket_api.async_register_command(hass, websocket_install_config_update) + websocket_api.async_register_command( + hass, websocket_subscribe_controller_statistics + ) + websocket_api.async_register_command(hass, websocket_subscribe_node_statistics) hass.http.register_view(DumpView()) hass.http.register_view(FirmwareUploadView()) @@ -1332,6 +1337,16 @@ async def websocket_abort_firmware_update( connection.send_result(msg[ID]) +def _get_firmware_update_progress_dict( + progress: FirmwareUpdateProgress, +) -> dict[str, int]: + """Get a dictionary of firmware update progress.""" + return { + "sent_fragments": progress.sent_fragments, + "total_fragments": progress.total_fragments, + } + + @websocket_api.require_admin @websocket_api.websocket_command( { @@ -1348,7 +1363,7 @@ async def websocket_subscribe_firmware_update_status( msg: dict, node: Node, ) -> None: - """Subsribe to the status of a firmware update.""" + """Subscribe to the status of a firmware update.""" @callback def async_cleanup() -> None: @@ -1364,8 +1379,7 @@ async def websocket_subscribe_firmware_update_status( msg[ID], { "event": event["event"], - "sent_fragments": progress.sent_fragments, - "total_fragments": progress.total_fragments, + **_get_firmware_update_progress_dict(progress), }, ) ) @@ -1390,15 +1404,10 @@ async def websocket_subscribe_firmware_update_status( ] connection.subscriptions[msg["id"]] = async_cleanup - result = ( - { - "sent_fragments": node.firmware_update_progress.sent_fragments, - "total_fragments": node.firmware_update_progress.total_fragments, - } - if node.firmware_update_progress - else None + progress = node.firmware_update_progress + connection.send_result( + msg[ID], _get_firmware_update_progress_dict(progress) if progress else None ) - connection.send_result(msg[ID], result) class FirmwareUploadView(HomeAssistantView): @@ -1495,3 +1504,126 @@ async def websocket_install_config_update( """Check for config updates.""" success = await client.driver.async_install_config_update() connection.send_result(msg[ID], success) + + +def _get_controller_statistics_dict( + statistics: ControllerStatistics, +) -> dict[str, int]: + """Get dictionary of controller statistics.""" + return { + "messages_tx": statistics.messages_tx, + "messages_rx": statistics.messages_rx, + "messages_dropped_tx": statistics.messages_dropped_tx, + "messages_dropped_rx": statistics.messages_dropped_rx, + "nak": statistics.nak, + "can": statistics.can, + "timeout_ack": statistics.timeout_ack, + "timout_response": statistics.timeout_response, + "timeout_callback": statistics.timeout_callback, + } + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_controller_statistics", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_subscribe_controller_statistics( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Subsribe to the statistics updates for a controller.""" + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_stats(event: dict) -> None: + statistics: ControllerStatistics = event["statistics_updated"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "source": "controller", + **_get_controller_statistics_dict(statistics), + }, + ) + ) + + controller = client.driver.controller + + msg[DATA_UNSUBSCRIBE] = unsubs = [ + controller.on("statistics updated", forward_stats) + ] + connection.subscriptions[msg["id"]] = async_cleanup + + connection.send_result( + msg[ID], _get_controller_statistics_dict(controller.statistics) + ) + + +def _get_node_statistics_dict(statistics: NodeStatistics) -> dict[str, int]: + """Get dictionary of node statistics.""" + return { + "commands_tx": statistics.commands_tx, + "commands_rx": statistics.commands_rx, + "commands_dropped_tx": statistics.commands_dropped_tx, + "commands_dropped_rx": statistics.commands_dropped_rx, + "timeout_response": statistics.timeout_response, + } + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_node_statistics", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_subscribe_node_statistics( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Subsribe to the statistics updates for a node.""" + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_stats(event: dict) -> None: + statistics: NodeStatistics = event["statistics_updated"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "source": "node", + "node_id": node.node_id, + **_get_node_statistics_dict(statistics), + }, + ) + ) + + msg[DATA_UNSUBSCRIBE] = unsubs = [node.on("statistics updated", forward_stats)] + connection.subscriptions[msg["id"]] = async_cleanup + + connection.send_result(msg[ID], _get_node_statistics_dict(node.statistics)) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index a96e76be865..75fca7f11ff 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2703,3 +2703,195 @@ async def test_install_config_update(hass, client, integration, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + + +async def test_subscribe_controller_statistics( + hass, integration, client, hass_ws_client +): + """Test the subscribe_controller_statistics command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_controller_statistics", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == { + "messages_tx": 0, + "messages_rx": 0, + "messages_dropped_tx": 0, + "messages_dropped_rx": 0, + "nak": 0, + "can": 0, + "timeout_ack": 0, + "timout_response": 0, + "timeout_callback": 0, + } + + # Fire statistics updated + event = Event( + "statistics updated", + { + "source": "controller", + "event": "statistics updated", + "statistics": { + "messagesTX": 1, + "messagesRX": 1, + "messagesDroppedTX": 1, + "messagesDroppedRX": 1, + "NAK": 1, + "CAN": 1, + "timeoutACK": 1, + "timeoutResponse": 1, + "timeoutCallback": 1, + }, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "statistics updated", + "source": "controller", + "messages_tx": 1, + "messages_rx": 1, + "messages_dropped_tx": 1, + "messages_dropped_rx": 1, + "nak": 1, + "can": 1, + "timeout_ack": 1, + "timout_response": 1, + "timeout_callback": 1, + } + + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_controller_statistics", + ENTRY_ID: "fake_entry_id", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_controller_statistics", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_subscribe_node_statistics( + hass, multisensor_6, integration, client, hass_ws_client +): + """Test the subscribe_node_statistics command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == { + "commands_tx": 0, + "commands_rx": 0, + "commands_dropped_tx": 0, + "commands_dropped_rx": 0, + "timeout_response": 0, + } + + # Fire statistics updated + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": multisensor_6.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 1, + "commandsDroppedTX": 1, + "commandsDroppedRX": 1, + "timeoutResponse": 1, + }, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "statistics updated", + "source": "node", + "node_id": multisensor_6.node_id, + "commands_tx": 1, + "commands_rx": 1, + "commands_dropped_tx": 1, + "commands_dropped_rx": 1, + "timeout_response": 1, + } + + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: "fake_entry_id", + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with improper node ID fails + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id + 100, + } + ) + msg = await ws_client.receive_json() + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED From fcc6ea7497e958a1b2a7fc94a46edc9fed282365 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 26 Jul 2021 18:37:37 +0200 Subject: [PATCH 592/818] Add energy integration (#52001) Co-authored-by: Paulus Schoutsen Co-authored-by: Erik --- .strict-typing | 1 + CODEOWNERS | 1 + .../components/default_config/manifest.json | 1 + homeassistant/components/energy/__init__.py | 25 ++ homeassistant/components/energy/const.py | 3 + homeassistant/components/energy/data.py | 264 ++++++++++++++++++ homeassistant/components/energy/manifest.json | 8 + homeassistant/components/energy/sensor.py | 258 +++++++++++++++++ homeassistant/components/energy/strings.json | 3 + .../components/energy/translations/en.json | 3 + .../components/energy/websocket_api.py | 116 ++++++++ .../components/forecast_solar/__init__.py | 6 +- homeassistant/components/history/__init__.py | 2 +- .../components/websocket_api/__init__.py | 2 + mypy.ini | 11 + tests/components/energy/__init__.py | 1 + tests/components/energy/test_sensor.py | 220 +++++++++++++++ tests/components/energy/test_websocket_api.py | 209 ++++++++++++++ tests/components/forecast_solar/test_init.py | 8 +- tests/components/history/test_init.py | 3 +- 20 files changed, 1137 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/energy/__init__.py create mode 100644 homeassistant/components/energy/const.py create mode 100644 homeassistant/components/energy/data.py create mode 100644 homeassistant/components/energy/manifest.json create mode 100644 homeassistant/components/energy/sensor.py create mode 100644 homeassistant/components/energy/strings.json create mode 100644 homeassistant/components/energy/translations/en.json create mode 100644 homeassistant/components/energy/websocket_api.py create mode 100644 tests/components/energy/__init__.py create mode 100644 tests/components/energy/test_sensor.py create mode 100644 tests/components/energy/test_websocket_api.py diff --git a/.strict-typing b/.strict-typing index 240be148a03..90d2d135de3 100644 --- a/.strict-typing +++ b/.strict-typing @@ -34,6 +34,7 @@ homeassistant.components.dsmr.* homeassistant.components.dunehd.* homeassistant.components.elgato.* homeassistant.components.esphome.* +homeassistant.components.energy.* homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* homeassistant.components.forecast_solar.* diff --git a/CODEOWNERS b/CODEOWNERS index 3979a3e4453..f872400c857 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -141,6 +141,7 @@ homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin homeassistant/components/emonitor/* @bdraco homeassistant/components/emulated_kasa/* @kbickar +homeassistant/components/energy/* @home_assistant/core homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/enphase_envoy/* @gtdiehl diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 032a6845340..834438f5a9f 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -7,6 +7,7 @@ "cloud", "counter", "dhcp", + "energy", "frontend", "history", "input_boolean", diff --git a/homeassistant/components/energy/__init__.py b/homeassistant/components/energy/__init__.py new file mode 100644 index 00000000000..1e060c1f35b --- /dev/null +++ b/homeassistant/components/energy/__init__.py @@ -0,0 +1,25 @@ +"""The Energy integration.""" +from __future__ import annotations + +from homeassistant.components import frontend +from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from . import websocket_api +from .const import DOMAIN + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Energy.""" + websocket_api.async_setup(hass) + frontend.async_register_built_in_panel(hass, DOMAIN, DOMAIN, "mdi:lightning-bolt") + + hass.async_create_task( + discovery.async_load_platform(hass, "sensor", DOMAIN, {}, config) + ) + hass.data[DOMAIN] = { + "cost_sensors": {}, + } + + return True diff --git a/homeassistant/components/energy/const.py b/homeassistant/components/energy/const.py new file mode 100644 index 00000000000..26093a93433 --- /dev/null +++ b/homeassistant/components/energy/const.py @@ -0,0 +1,3 @@ +"""Constants for the Energy integration.""" + +DOMAIN = "energy" diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py new file mode 100644 index 00000000000..7e867e36bfc --- /dev/null +++ b/homeassistant/components/energy/data.py @@ -0,0 +1,264 @@ +"""Energy data.""" +from __future__ import annotations + +import asyncio +from collections import Counter +from collections.abc import Awaitable +from typing import Callable, Literal, Optional, TypedDict, Union, cast + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, singleton, storage + +from .const import DOMAIN + +STORAGE_VERSION = 1 +STORAGE_KEY = DOMAIN + + +@singleton.singleton(f"{DOMAIN}_manager") +async def async_get_manager(hass: HomeAssistant) -> EnergyManager: + """Return an initialized data manager.""" + manager = EnergyManager(hass) + await manager.async_initialize() + return manager + + +class FlowFromGridSourceType(TypedDict): + """Dictionary describing the 'from' stat for the grid source.""" + + # statistic_id of a an energy meter (kWh) + stat_energy_from: str + + # statistic_id of costs ($) incurred from the energy meter + # If set to None and entity_energy_from and entity_energy_price are configured, + # an EnergyCostSensor will be automatically created + stat_cost: str | None + + # Used to generate costs if stat_cost is set to None + entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from + entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) + number_energy_price: float | None # Price for energy ($/kWh) + + +class FlowToGridSourceType(TypedDict): + """Dictionary describing the 'to' stat for the grid source.""" + + # kWh meter + stat_energy_to: str + + # statistic_id of compensation ($) received for contributing back + # If set to None and entity_energy_from and entity_energy_price are configured, + # an EnergyCostSensor will be automatically created + stat_compensation: str | None + + # Used to generate costs if stat_compensation is set to None + entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from + entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) + number_energy_price: float | None # Price for energy ($/kWh) + + +class GridSourceType(TypedDict): + """Dictionary holding the source of grid energy consumption.""" + + type: Literal["grid"] + + flow_from: list[FlowFromGridSourceType] + flow_to: list[FlowToGridSourceType] + + cost_adjustment_day: float + + +class SolarSourceType(TypedDict): + """Dictionary holding the source of energy production.""" + + type: Literal["solar"] + + stat_energy_from: str + config_entry_solar_forecast: list[str] | None + + +SourceType = Union[GridSourceType, SolarSourceType] + + +class DeviceConsumption(TypedDict): + """Dictionary holding the source of individual device consumption.""" + + # This is an ever increasing value + stat_consumption: str + + +class EnergyPreferences(TypedDict): + """Dictionary holding the energy data.""" + + currency: str + energy_sources: list[SourceType] + device_consumption: list[DeviceConsumption] + + +class EnergyPreferencesUpdate(EnergyPreferences, total=False): + """all types optional.""" + + +def _flow_from_ensure_single_price( + val: FlowFromGridSourceType, +) -> FlowFromGridSourceType: + """Ensure we use a single price source.""" + if ( + val["entity_energy_price"] is not None + and val["number_energy_price"] is not None + ): + raise vol.Invalid("Define either an entity or a fixed number for the price") + + return val + + +FLOW_FROM_GRID_SOURCE_SCHEMA = vol.All( + vol.Schema( + { + vol.Required("stat_energy_from"): str, + vol.Optional("stat_cost"): vol.Any(str, None), + vol.Optional("entity_energy_from"): vol.Any(str, None), + vol.Optional("entity_energy_price"): vol.Any(str, None), + vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), + } + ), + _flow_from_ensure_single_price, +) + + +FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("stat_energy_to"): str, + vol.Optional("stat_compensation"): vol.Any(str, None), + vol.Optional("entity_energy_to"): vol.Any(str, None), + vol.Optional("entity_energy_price"): vol.Any(str, None), + vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), + } +) + + +def _generate_unique_value_validator(key: str) -> Callable[[list[dict]], list[dict]]: + """Generate a validator that ensures a value is only used once.""" + + def validate_uniqueness( + val: list[dict], + ) -> list[dict]: + """Ensure that the user doesn't add duplicate values.""" + counts = Counter(flow_from[key] for flow_from in val) + + for value, count in counts.items(): + if count > 1: + raise vol.Invalid(f"Cannot specify {value} more than once") + + return val + + return validate_uniqueness + + +GRID_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("type"): "grid", + vol.Required("flow_from"): vol.All( + [FLOW_FROM_GRID_SOURCE_SCHEMA], + _generate_unique_value_validator("stat_energy_from"), + ), + vol.Required("flow_to"): vol.All( + [FLOW_TO_GRID_SOURCE_SCHEMA], + _generate_unique_value_validator("stat_energy_to"), + ), + vol.Required("cost_adjustment_day"): vol.Coerce(float), + } +) +SOLAR_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("type"): "solar", + vol.Required("stat_energy_from"): str, + vol.Optional("config_entry_solar_forecast"): vol.Any([str], None), + } +) + + +def check_type_limits(value: list[SourceType]) -> list[SourceType]: + """Validate that we don't have too many of certain types.""" + types = Counter([val["type"] for val in value]) + + if types.get("grid", 0) > 1: + raise vol.Invalid("You cannot have more than 1 grid source") + + return value + + +ENERGY_SOURCE_SCHEMA = vol.All( + vol.Schema( + [ + cv.key_value_schemas( + "type", + { + "grid": GRID_SOURCE_SCHEMA, + "solar": SOLAR_SOURCE_SCHEMA, + }, + ) + ] + ), + check_type_limits, +) + +DEVICE_CONSUMPTION_SCHEMA = vol.Schema( + { + vol.Required("stat_consumption"): str, + } +) + + +class EnergyManager: + """Manage the instance energy prefs.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize energy manager.""" + self._hass = hass + self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self.data: EnergyPreferences | None = None + self._update_listeners: list[Callable[[], Awaitable]] = [] + + async def async_initialize(self) -> None: + """Initialize the energy integration.""" + self.data = cast(Optional[EnergyPreferences], await self._store.async_load()) + + @staticmethod + def default_preferences() -> EnergyPreferences: + """Return default preferences.""" + return { + "currency": "€", + "energy_sources": [], + "device_consumption": [], + } + + async def async_update(self, update: EnergyPreferencesUpdate) -> None: + """Update the preferences.""" + if self.data is None: + data = EnergyManager.default_preferences() + else: + data = self.data.copy() + + for key in ( + "currency", + "energy_sources", + "device_consumption", + ): + if key in update: + data[key] = update[key] # type: ignore + + self.data = data + self._store.async_delay_save(lambda: cast(dict, self.data), 60) + + if not self._update_listeners: + return + + await asyncio.gather(*(listener() for listener in self._update_listeners)) + + @callback + def async_listen_updates(self, update_listener: Callable[[], Awaitable]) -> None: + """Listen for data updates.""" + self._update_listeners.append(update_listener) diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json new file mode 100644 index 00000000000..eca02615454 --- /dev/null +++ b/homeassistant/components/energy/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "energy", + "name": "Energy", + "documentation": "https://www.home-assistant.io/integrations/energy", + "codeowners": ["@home_assistant/core"], + "iot_class": "calculated", + "dependencies": ["websocket_api", "history"] +} diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py new file mode 100644 index 00000000000..1a4dd4b7e41 --- /dev/null +++ b/homeassistant/components/energy/sensor.py @@ -0,0 +1,258 @@ +"""Helper sensor for calculating utility costs.""" +from __future__ import annotations + +from dataclasses import dataclass +from functools import partial +from typing import Any, Final, Literal, TypeVar, cast + +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + DEVICE_CLASS_MONETARY, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) +from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .data import EnergyManager, async_get_manager + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the energy sensors.""" + manager = await async_get_manager(hass) + process_now = partial(_process_manager_data, hass, manager, async_add_entities, {}) + manager.async_listen_updates(process_now) + + if manager.data: + await process_now() + + +T = TypeVar("T") + + +@dataclass +class FlowAdapter: + """Adapter to allow flows to be used as sensors.""" + + flow_type: Literal["flow_from", "flow_to"] + stat_energy_key: Literal["stat_energy_from", "stat_energy_to"] + entity_energy_key: Literal["entity_energy_from", "entity_energy_to"] + total_money_key: Literal["stat_cost", "stat_compensation"] + name_suffix: str + entity_id_suffix: str + + +FLOW_ADAPTERS: Final = ( + FlowAdapter( + "flow_from", + "stat_energy_from", + "entity_energy_from", + "stat_cost", + "Cost", + "cost", + ), + FlowAdapter( + "flow_to", + "stat_energy_to", + "entity_energy_to", + "stat_compensation", + "Compensation", + "compensation", + ), +) + + +async def _process_manager_data( + hass: HomeAssistant, + manager: EnergyManager, + async_add_entities: AddEntitiesCallback, + current_entities: dict[tuple[str, str], EnergyCostSensor], +) -> None: + """Process updated data.""" + to_add: list[SensorEntity] = [] + to_remove = dict(current_entities) + + async def finish() -> None: + if to_add: + async_add_entities(to_add) + + for key, entity in to_remove.items(): + current_entities.pop(key) + await entity.async_remove() + + if not manager.data: + await finish() + return + + for energy_source in manager.data["energy_sources"]: + if energy_source["type"] != "grid": + continue + + for adapter in FLOW_ADAPTERS: + for flow in energy_source[adapter.flow_type]: + # Opting out of the type complexity because can't get it to work + untyped_flow = cast(dict, flow) + + # No need to create an entity if we already have a cost stat + if untyped_flow.get(adapter.total_money_key) is not None: + continue + + # This is unique among all flow_from's + key = (adapter.flow_type, untyped_flow[adapter.stat_energy_key]) + + # Make sure the right data is there + # If the entity existed, we don't pop it from to_remove so it's removed + if untyped_flow.get(adapter.entity_energy_key) is None or ( + untyped_flow.get("entity_energy_price") is None + and untyped_flow.get("number_energy_price") is None + ): + continue + + current_entity = to_remove.pop(key, None) + if current_entity: + current_entity.update_config(untyped_flow) + continue + + current_entities[key] = EnergyCostSensor( + adapter, + manager.data["currency"], + untyped_flow, + ) + to_add.append(current_entities[key]) + + await finish() + + +class EnergyCostSensor(SensorEntity): + """Calculate costs incurred by consuming energy. + + This is intended as a fallback for when no specific cost sensor is available for the + utility. + """ + + def __init__( + self, + adapter: FlowAdapter, + currency: str, + flow: dict, + ) -> None: + """Initialize the sensor.""" + super().__init__() + + self._adapter = adapter + self.entity_id = f"{flow[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" + self._attr_device_class = DEVICE_CLASS_MONETARY + self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_unit_of_measurement = currency + self._flow = flow + self._last_energy_sensor_state: State | None = None + + def _reset(self, energy_state: State) -> None: + """Reset the cost sensor.""" + self._attr_state = 0.0 + self._attr_last_reset = dt_util.utcnow() + self._last_energy_sensor_state = energy_state + self.async_write_ha_state() + + @callback + def _update_cost(self) -> None: + """Update incurred costs.""" + energy_state = self.hass.states.get( + cast(str, self._flow[self._adapter.entity_energy_key]) + ) + + if energy_state is None or ATTR_LAST_RESET not in energy_state.attributes: + return + + try: + energy = float(energy_state.state) + except ValueError: + return + + # Determine energy price + if self._flow["entity_energy_price"] is not None: + energy_price_state = self.hass.states.get(self._flow["entity_energy_price"]) + + if energy_price_state is None: + return + + try: + energy_price = float(energy_price_state.state) + except ValueError: + return + else: + energy_price_state = None + energy_price = cast(float, self._flow["number_energy_price"]) + + if self._last_energy_sensor_state is None: + # Initialize as it's the first time all required entities are in place. + self._reset(energy_state) + return + + cur_value = cast(float, self._attr_state) + if ( + energy_state.attributes[ATTR_LAST_RESET] + != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] + ): + # Energy meter was reset, reset cost sensor too + self._reset(energy_state) + else: + # Update with newly incurred cost + old_energy_value = float(self._last_energy_sensor_state.state) + self._attr_state = cur_value + (energy - old_energy_value) * energy_price + + self._last_energy_sensor_state = energy_state + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + energy_state = self.hass.states.get(self._flow[self._adapter.entity_energy_key]) + if energy_state: + name = energy_state.name + else: + name = split_entity_id(self._flow[self._adapter.entity_energy_key])[ + 0 + ].replace("_", " ") + + self._attr_name = f"{name} {self._adapter.name_suffix}" + + self._update_cost() + + # Store stat ID in hass.data so frontend can look it up + self.hass.data[DOMAIN]["cost_sensors"][ + self._flow[self._adapter.entity_energy_key] + ] = self.entity_id + + @callback + def async_state_changed_listener(*_: Any) -> None: + """Handle child updates.""" + self._update_cost() + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, + cast(str, self._flow[self._adapter.entity_energy_key]), + async_state_changed_listener, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Handle removing from hass.""" + self.hass.data[DOMAIN]["cost_sensors"].pop( + self._flow[self._adapter.entity_energy_key] + ) + await super().async_will_remove_from_hass() + + @callback + def update_config(self, flow: dict) -> None: + """Update the config.""" + self._flow = flow diff --git a/homeassistant/components/energy/strings.json b/homeassistant/components/energy/strings.json new file mode 100644 index 00000000000..6cdcd827633 --- /dev/null +++ b/homeassistant/components/energy/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Energy" +} diff --git a/homeassistant/components/energy/translations/en.json b/homeassistant/components/energy/translations/en.json new file mode 100644 index 00000000000..109e1bd5af8 --- /dev/null +++ b/homeassistant/components/energy/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Energy" +} \ No newline at end of file diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py new file mode 100644 index 00000000000..53e4aa7714c --- /dev/null +++ b/homeassistant/components/energy/websocket_api.py @@ -0,0 +1,116 @@ +"""The Energy websocket API.""" +from __future__ import annotations + +import asyncio +import functools +from typing import Any, Awaitable, Callable, Dict, cast + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .data import ( + DEVICE_CONSUMPTION_SCHEMA, + ENERGY_SOURCE_SCHEMA, + EnergyManager, + EnergyPreferencesUpdate, + async_get_manager, +) + +EnergyWebSocketCommandHandler = Callable[ + [HomeAssistant, websocket_api.ActiveConnection, Dict[str, Any], "EnergyManager"], + None, +] +AsyncEnergyWebSocketCommandHandler = Callable[ + [HomeAssistant, websocket_api.ActiveConnection, Dict[str, Any], "EnergyManager"], + Awaitable[None], +] + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the energy websocket API.""" + websocket_api.async_register_command(hass, ws_get_prefs) + websocket_api.async_register_command(hass, ws_save_prefs) + websocket_api.async_register_command(hass, ws_info) + + +def _ws_with_manager( + func: Any, +) -> websocket_api.WebSocketCommandHandler: + """Decorate a function to pass in a manager.""" + + @websocket_api.async_response + @functools.wraps(func) + async def with_manager( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + manager = await async_get_manager(hass) + + result = func(hass, connection, msg, manager) + + if asyncio.iscoroutine(result): + await result + + return with_manager + + +@websocket_api.websocket_command( + { + vol.Required("type"): "energy/get_prefs", + } +) +@_ws_with_manager +@callback +def ws_get_prefs( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, + manager: EnergyManager, +) -> None: + """Handle get prefs command.""" + if manager.data is None: + connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "No prefs") + return + + connection.send_result(msg["id"], manager.data) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "energy/save_prefs", + vol.Optional("currency"): str, + vol.Optional("energy_sources"): ENERGY_SOURCE_SCHEMA, + vol.Optional("device_consumption"): [DEVICE_CONSUMPTION_SCHEMA], + } +) +@_ws_with_manager +async def ws_save_prefs( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, + manager: EnergyManager, +) -> None: + """Handle get prefs command.""" + msg_id = msg.pop("id") + msg.pop("type") + await manager.async_update(cast(EnergyPreferencesUpdate, msg)) + connection.send_result(msg_id, manager.data) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "energy/info", + } +) +@callback +def ws_info( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Handle get info command.""" + connection.send_result(msg["id"], hass.data[DOMAIN]) diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index b20a0befb96..4d996736ecf 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -93,8 +93,10 @@ def ws_list_forecasts( for config_entry_id, coordinator in hass.data[DOMAIN].items(): forecasts[config_entry_id] = { - timestamp.isoformat(): val - for timestamp, val in coordinator.data.watts.items() + "wh_hours": { + timestamp.isoformat(): val + for timestamp, val in coordinator.data.wh_hours.items() + } } connection.send_result(msg["id"], forecasts) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 8fa9fe879f5..3651dd8295f 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -164,7 +164,7 @@ async def ws_get_statistics_during_period( @websocket_api.websocket_command( { vol.Required("type"): "history/list_statistic_ids", - vol.Optional("statistic_type"): str, + vol.Optional("statistic_type"): vol.Any("sum", "mean"), } ) @websocket_api.require_admin diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 52158d3f1ad..2e44a0aa0cd 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -21,6 +21,8 @@ from .const import ( # noqa: F401 ERR_UNAUTHORIZED, ERR_UNKNOWN_COMMAND, ERR_UNKNOWN_ERROR, + AsyncWebSocketCommandHandler, + WebSocketCommandHandler, ) from .decorators import ( # noqa: F401 async_response, diff --git a/mypy.ini b/mypy.ini index 5c3b2835f72..beab8cd8d17 100644 --- a/mypy.ini +++ b/mypy.ini @@ -385,6 +385,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.energy.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fastdotcom.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/energy/__init__.py b/tests/components/energy/__init__.py new file mode 100644 index 00000000000..ca14c80b951 --- /dev/null +++ b/tests/components/energy/__init__.py @@ -0,0 +1 @@ +"""Tests for the Energy integration.""" diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py new file mode 100644 index 00000000000..b6e0fc77188 --- /dev/null +++ b/tests/components/energy/test_sensor.py @@ -0,0 +1,220 @@ +"""Test the Energy sensors.""" +import copy +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components.energy import data +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, +) +from homeassistant.components.sensor.recorder import compile_statistics +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_MONETARY, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def setup_integration(hass): + """Set up the integration.""" + assert await async_setup_component( + hass, "energy", {"recorder": {"db_url": "sqlite://"}} + ) + await hass.async_block_till_done() + + +async def test_cost_sensor_no_states(hass, hass_storage) -> None: + """Test sensors are created.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "foo", + "entity_energy_from": "foo", + "stat_cost": None, + "entity_energy_price": "bar", + "number_energy_price": None, + } + ], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + await setup_integration(hass) + # TODO: No states, should the cost entity refuse to setup? + + +@pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")]) +@pytest.mark.parametrize( + "price_entity,fixed_price", [("sensor.energy_price", None), (None, 1)] +) +@pytest.mark.parametrize( + "usage_sensor_entity_id,cost_sensor_entity_id,flow_type", + [ + ("sensor.energy_consumption", "sensor.energy_consumption_cost", "flow_from"), + ( + "sensor.energy_production", + "sensor.energy_production_compensation", + "flow_to", + ), + ], +) +async def test_cost_sensor_price_entity( + hass, + hass_storage, + hass_ws_client, + initial_energy, + initial_cost, + price_entity, + fixed_price, + usage_sensor_entity_id, + cost_sensor_entity_id, + flow_type, +) -> None: + """Test energy cost price from sensor entity.""" + + def _compile_statistics(_): + return compile_statistics(hass, now, now + timedelta(seconds=1)) + + await async_init_recorder_component(hass) + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.energy_consumption", + "entity_energy_from": "sensor.energy_consumption", + "stat_cost": None, + "entity_energy_price": price_entity, + "number_energy_price": fixed_price, + } + ] + if flow_type == "flow_from" + else [], + "flow_to": [ + { + "stat_energy_to": "sensor.energy_production", + "entity_energy_to": "sensor.energy_production", + "stat_compensation": None, + "entity_energy_price": price_entity, + "number_energy_price": fixed_price, + } + ] + if flow_type == "flow_to" + else [], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + + now = dt_util.utcnow() + last_reset = dt_util.utc_from_timestamp(0).isoformat() + + # Optionally initialize dependent entities + if initial_energy is not None: + hass.states.async_set( + usage_sensor_entity_id, initial_energy, {"last_reset": last_reset} + ) + hass.states.async_set("sensor.energy_price", "1") + + with patch("homeassistant.util.dt.utcnow", return_value=now): + await setup_integration(hass) + + state = hass.states.get(cost_sensor_entity_id) + assert state.state == initial_cost + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + if initial_cost != "unknown": + assert state.attributes[ATTR_LAST_RESET] == now.isoformat() + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "€" + + # Optional late setup of dependent entities + if initial_energy is None: + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set( + usage_sensor_entity_id, "0", {"last_reset": last_reset} + ) + await hass.async_block_till_done() + + state = hass.states.get(cost_sensor_entity_id) + assert state.state == "0.0" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_LAST_RESET] == now.isoformat() + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "€" + + # # Unique ID temp disabled + # # entity_registry = er.async_get(hass) + # # entry = entity_registry.async_get(cost_sensor_entity_id) + # # assert entry.unique_id == "energy_energy_consumption cost" + + # Energy use bumped to 10 kWh + hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) + await hass.async_block_till_done() + state = hass.states.get(cost_sensor_entity_id) + assert state.state == "10.0" # 0 € + (10-0) kWh * 1 €/kWh = 10 € + + # Nothing happens when price changes + if price_entity is not None: + hass.states.async_set(price_entity, "2") + await hass.async_block_till_done() + else: + energy_data = copy.deepcopy(energy_data) + energy_data["energy_sources"][0][flow_type][0]["number_energy_price"] = 2 + client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "energy/save_prefs", **energy_data}) + msg = await client.receive_json() + assert msg["success"] + state = hass.states.get(cost_sensor_entity_id) + assert state.state == "10.0" # 10 € + (10-10) kWh * 2 €/kWh = 10 € + + # Additional consumption is using the new price + hass.states.async_set(usage_sensor_entity_id, "14.5", {"last_reset": last_reset}) + await hass.async_block_till_done() + state = hass.states.get(cost_sensor_entity_id) + assert state.state == "19.0" # 10 € + (14.5-10) kWh * 2 €/kWh = 19 € + + # Check generated statistics + await async_wait_recording_done_without_instance(hass) + statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) + assert cost_sensor_entity_id in statistics + assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0 + + # Energy sensor is reset, with start point at 4kWh + last_reset = (now + timedelta(seconds=1)).isoformat() + hass.states.async_set(usage_sensor_entity_id, "4", {"last_reset": last_reset}) + await hass.async_block_till_done() + state = hass.states.get(cost_sensor_entity_id) + assert state.state == "0.0" # 0 € + (4-4) kWh * 2 €/kWh = 0 € + + # Energy use bumped to 10 kWh + hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) + await hass.async_block_till_done() + state = hass.states.get(cost_sensor_entity_id) + assert state.state == "12.0" # 0 € + (10-4) kWh * 2 €/kWh = 12 € + + # Check generated statistics + await async_wait_recording_done_without_instance(hass) + statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) + assert cost_sensor_entity_id in statistics + assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 31.0 diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py new file mode 100644 index 00000000000..a37850dd566 --- /dev/null +++ b/tests/components/energy/test_websocket_api.py @@ -0,0 +1,209 @@ +"""Test the Energy websocket API.""" +import pytest + +from homeassistant.components.energy import data +from homeassistant.setup import async_setup_component + +from tests.common import flush_store + + +@pytest.fixture(autouse=True) +async def setup_integration(hass): + """Set up the integration.""" + assert await async_setup_component( + hass, "energy", {"recorder": {"db_url": "sqlite://"}} + ) + + +async def test_get_preferences_no_data(hass, hass_ws_client) -> None: + """Test we get error if no preferences set.""" + client = await hass_ws_client(hass) + + await client.send_json({"id": 5, "type": "energy/get_prefs"}) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"] == {"code": "not_found", "message": "No prefs"} + + +async def test_get_preferences_default(hass, hass_ws_client, hass_storage) -> None: + """Test we get preferences.""" + manager = await data.async_get_manager(hass) + manager.data = data.EnergyManager.default_preferences() + client = await hass_ws_client(hass) + + await client.send_json({"id": 5, "type": "energy/get_prefs"}) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == data.EnergyManager.default_preferences() + + +async def test_save_preferences(hass, hass_ws_client, hass_storage) -> None: + """Test we can save preferences.""" + client = await hass_ws_client(hass) + + # Test saving default prefs is also valid. + default_prefs = data.EnergyManager.default_preferences() + + await client.send_json({"id": 5, "type": "energy/save_prefs", **default_prefs}) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == default_prefs + + new_prefs = { + "currency": "$", + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.heat_pump_meter", + "stat_cost": "heat_pump_kwh_cost", + "entity_energy_from": None, + "entity_energy_price": None, + "number_energy_price": None, + }, + { + "stat_energy_from": "sensor.heat_pump_meter_2", + "stat_cost": None, + "entity_energy_from": "sensor.heat_pump_meter_2", + "entity_energy_price": None, + "number_energy_price": 0.20, + }, + ], + "flow_to": [ + { + "stat_energy_to": "sensor.return_to_grid_peak", + "stat_compensation": None, + "entity_energy_to": None, + "entity_energy_price": None, + "number_energy_price": None, + }, + { + "stat_energy_to": "sensor.return_to_grid_offpeak", + "stat_compensation": None, + "entity_energy_to": "sensor.return_to_grid_offpeak", + "entity_energy_price": None, + "number_energy_price": 0.20, + }, + ], + "cost_adjustment_day": 1.2, + }, + { + "type": "solar", + "stat_energy_from": "my_solar_production", + "config_entry_solar_forecast": ["predicted_config_entry"], + }, + ], + "device_consumption": [{"stat_consumption": "some_device_usage"}], + } + + await client.send_json({"id": 6, "type": "energy/save_prefs", **new_prefs}) + + msg = await client.receive_json() + + assert msg["id"] == 6 + assert msg["success"] + assert msg["result"] == new_prefs + + assert data.STORAGE_KEY not in hass_storage, "expected not to be written yet" + + await flush_store((await data.async_get_manager(hass))._store) + + assert hass_storage[data.STORAGE_KEY]["data"] == new_prefs + + # Verify info reflects data. + await client.send_json({"id": 7, "type": "energy/info"}) + + msg = await client.receive_json() + + assert msg["id"] == 7 + assert msg["success"] + assert msg["result"] == { + "cost_sensors": { + "sensor.heat_pump_meter_2": "sensor.heat_pump_meter_2_cost", + "sensor.return_to_grid_offpeak": "sensor.return_to_grid_offpeak_compensation", + } + } + + # Prefs with limited options + new_prefs_2 = { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.heat_pump_meter", + "stat_cost": None, + "entity_energy_from": None, + "entity_energy_price": None, + "number_energy_price": None, + } + ], + "flow_to": [], + "cost_adjustment_day": 1.2, + }, + { + "type": "solar", + "stat_energy_from": "my_solar_production", + "config_entry_solar_forecast": None, + }, + ], + } + + await client.send_json({"id": 8, "type": "energy/save_prefs", **new_prefs_2}) + + msg = await client.receive_json() + + assert msg["id"] == 8 + assert msg["success"] + assert msg["result"] == {**new_prefs, **new_prefs_2} + + +async def test_handle_duplicate_from_stat(hass, hass_ws_client) -> None: + """Test we handle duplicate from stats.""" + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 5, + "type": "energy/save_prefs", + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.heat_pump_meter", + "stat_cost": None, + "entity_energy_from": None, + "entity_energy_price": None, + "number_energy_price": None, + }, + { + "stat_energy_from": "sensor.heat_pump_meter", + "stat_cost": None, + "entity_energy_from": None, + "entity_energy_price": None, + "number_energy_price": None, + }, + ], + "flow_to": [], + "cost_adjustment_day": 0, + }, + ], + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index 7544f7d352b..453196e3300 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -18,7 +18,7 @@ async def test_load_unload_config_entry( hass_ws_client, ) -> None: """Test the Forecast.Solar configuration entry loading/unloading.""" - mock_forecast_solar.estimate.return_value.watts = { + mock_forecast_solar.estimate.return_value.wh_hours = { datetime(2021, 6, 27, 13, 0, tzinfo=timezone.utc): 12, datetime(2021, 6, 27, 14, 0, tzinfo=timezone.utc): 8, } @@ -41,8 +41,10 @@ async def test_load_unload_config_entry( assert result["success"] assert result["result"] == { mock_config_entry.entry_id: { - "2021-06-27T13:00:00+00:00": 12, - "2021-06-27T14:00:00+00:00": 8, + "wh_hours": { + "2021-06-27T13:00:00+00:00": 12, + "2021-06-27T14:00:00+00:00": 8, + } } } diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 8d78f80c634..7909d8f0239 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1025,8 +1025,7 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "dogs"} ) response = await client.receive_json() - assert response["success"] - assert response["result"] == [] + assert not response["success"] await client.send_json( {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "mean"} From f28d8fae2fc62e69b96ad3ebafe1e9da3196b5de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Jul 2021 19:21:40 +0200 Subject: [PATCH 593/818] Mark energy integration as internal (#53513) --- homeassistant/components/energy/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index eca02615454..ee6fd784823 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/energy", "codeowners": ["@home_assistant/core"], "iot_class": "calculated", - "dependencies": ["websocket_api", "history"] + "dependencies": ["websocket_api", "history"], + "quality_scale": "internal" } From 962ddf664c3dcb2f439898aa550143687a10ef5b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Jul 2021 11:10:39 -0700 Subject: [PATCH 594/818] Add country code to co2signal state attributes (#53512) --- homeassistant/components/co2signal/sensor.py | 5 ++++- tests/components/co2signal/test_config_flow.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 0b79378c36b..e7ac138335b 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -100,7 +100,10 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE name = f"{extra_name} - {name}" self._attr_name = name - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_extra_state_attributes = { + "country_code": coordinator.data["countryCode"], + ATTR_ATTRIBUTION: ATTRIBUTION, + } self._attr_device_info = { ATTR_IDENTIFIERS: {(DOMAIN, coordinator.entry_id)}, ATTR_NAME: "CO2 signal", diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 129ab7124fe..668c72e9b04 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -218,12 +218,14 @@ async def test_import(hass: HomeAssistant) -> None: assert state.state == "45.99" assert state.name == "CO2 intensity" assert state.attributes["unit_of_measurement"] == "gCO2eq/kWh" + assert state.attributes["country_code"] == "FR" state = hass.states.get("sensor.grid_fossil_fuel_percentage") assert state is not None assert state.state == "5.46" assert state.name == "Grid fossil fuel percentage" assert state.attributes["unit_of_measurement"] == "%" + assert state.attributes["country_code"] == "FR" async def test_import_abort_existing_home(hass: HomeAssistant) -> None: From 23b64cd4967602b4d4a801ba2d5aa14119863c23 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 26 Jul 2021 11:43:22 -0700 Subject: [PATCH 595/818] Bump motioneye-client version to v0.3.11 (#53504) --- homeassistant/components/motioneye/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 59c09097223..9be95c21162 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -8,10 +8,10 @@ "webhook" ], "requirements": [ - "motioneye-client==0.3.10" + "motioneye-client==0.3.11" ], "codeowners": [ "@dermotduffy" ], "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 47bc55074be..c348e03b06a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ mitemp_bt==0.0.3 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.10 +motioneye-client==0.3.11 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f05af08862e..6c3fe00b068 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ minio==4.0.9 motionblinds==0.4.10 # homeassistant.components.motioneye -motioneye-client==0.3.10 +motioneye-client==0.3.11 # homeassistant.components.mullvad mullvad-api==1.0.0 From f4d65e3751a0532498aa5e1177b571ba5ce367e1 Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 26 Jul 2021 20:48:20 +0200 Subject: [PATCH 596/818] Musiccast grouping fixes (#52339) Co-authored-by: Martin Hjelmare --- .../components/yamaha_musiccast/manifest.json | 2 +- .../yamaha_musiccast/media_player.py | 122 ++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 69 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 501e3b8a00b..46fae870e5e 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.8.0" + "aiomusiccast==0.8.2" ], "ssdp": [ { diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index c3269df6ca5..77bc7a21b85 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -34,6 +34,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -148,22 +149,28 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self._cur_track = 0 self._repeat = REPEAT_MODE_OFF - self.coordinator.entities.append(self) async def async_added_to_hass(self): """Run when this Entity has been added to HA.""" await super().async_added_to_hass() + self.coordinator.entities.append(self) # Sensors should also register callbacks to HA when their state changes self.coordinator.musiccast.register_callback(self.async_write_ha_state) self.coordinator.musiccast.register_group_update_callback( self.update_all_mc_entities ) + self.coordinator.async_add_listener(self.async_schedule_check_client_list) async def async_will_remove_from_hass(self): """Entity being removed from hass.""" await super().async_will_remove_from_hass() + self.coordinator.entities.remove(self) # The opposite of async_added_to_hass. Remove any registered call backs here. self.coordinator.musiccast.remove_callback(self.async_write_ha_state) + self.coordinator.musiccast.remove_group_update_callback( + self.update_all_mc_entities + ) + self.coordinator.async_remove_listener(self.async_schedule_check_client_list) @property def should_poll(self): @@ -571,12 +578,18 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return self - async def update_all_mc_entities(self): + async def update_all_mc_entities(self, check_clients=False): """Update the whole musiccast system when group data change.""" - for entity in self.get_all_mc_entities(): - if entity.is_server: + # First update all servers as they provide the group information for their clients + for entity in self.get_all_server_entities(): + if check_clients or self.coordinator.musiccast.group_reduce_by_source: await entity.async_check_client_list() - entity.async_write_ha_state() + else: + entity.async_write_ha_state() + # Then update all other entities + for entity in self.get_all_mc_entities(): + if not entity.is_server: + entity.async_write_ha_state() # Services @@ -585,7 +598,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): Creates a new group if necessary. Used for join service. """ - _LOGGER.info( + _LOGGER.debug( "%s wants to add the following entities %s", self.entity_id, str(group_members), @@ -597,6 +610,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): if entity.entity_id in group_members ] + if self.state == STATE_OFF: + await self.async_turn_on() + if not self.is_server and self.musiccast_zone_entity.is_server: # The MusicCast Distribution Module of this device is already in use. To use it as a server, we first # have to unjoin and wait until the servers are updated. @@ -609,38 +625,40 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): if self.is_server else uuid.random_uuid_hex().upper() ) + + ip_addresses = set() # First let the clients join for client in entities: if client != self: try: - await client.async_client_join(group, self) + network_join = await client.async_client_join(group, self) except MusicCastGroupException: _LOGGER.warning( "%s is struggling to update its group data. Will retry perform the update", client.entity_id, ) - await client.async_client_join(group, self) + network_join = await client.async_client_join(group, self) - await self.coordinator.musiccast.mc_server_group_extend( - self._zone_id, - [ - entity.ip_address - for entity in entities - if entity.ip_address != self.ip_address - ], - group, - self.get_distribution_num(), - ) + if network_join: + ip_addresses.add(client.ip_address) + + if ip_addresses: + await self.coordinator.musiccast.mc_server_group_extend( + self._zone_id, + list(ip_addresses), + group, + self.get_distribution_num(), + ) _LOGGER.debug( "%s added the following entities %s", self.entity_id, str(entities) ) - _LOGGER.info( + _LOGGER.debug( "%s has now the following musiccast group %s", self.entity_id, str(self.musiccast_group), ) - await self.update_all_mc_entities() + await self.update_all_mc_entities(True) async def async_unjoin_player(self): """Leave the group. @@ -654,15 +672,15 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): else: await self.async_client_leave_group() - await self.update_all_mc_entities() + await self.update_all_mc_entities(True) # Internal client functions - async def async_client_join(self, group_id, server): + async def async_client_join(self, group_id, server) -> bool: """Let the client join a group. If this client is a server, the server will stop distributing. If the client is part of a different group, - it will leave that group first. + it will leave that group first. Returns True, if the server has to add the client on his side. """ # If we should join the group, which is served by the main zone, we can simply select main_sync as input. _LOGGER.debug("%s called service client join", self.entity_id) @@ -672,14 +690,16 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): if server.zone == DEFAULT_ZONE: await self.async_select_source(ATTR_MAIN_SYNC) server.async_write_ha_state() - return + return False # It is not possible to join a group hosted by zone2 from main zone. - raise Exception("Can not join a zone other than main of the same device.") + raise HomeAssistantError( + "Can not join a zone other than main of the same device." + ) if self.musiccast_zone_entity.is_server: # If one of the zones of the device is a server, we need to unjoin first. - _LOGGER.info( + _LOGGER.debug( "%s is a server of a group and has to stop distribution " "to use MusicCast for %s", self.musiccast_zone_entity.entity_id, @@ -688,11 +708,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): await self.musiccast_zone_entity.async_server_close_group() elif self.is_client: - if self.coordinator.data.group_id == server.coordinator.data.group_id: + if self.is_part_of_group(server): _LOGGER.warning("%s is already part of the group", self.entity_id) - return + return False - _LOGGER.info( + _LOGGER.debug( "%s is client in a different group, will unjoin first", self.entity_id, ) @@ -705,20 +725,14 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ): # The device is already part of this group (e.g. main zone is also a client of this group). # Just select mc_link as source - await self.async_select_source(ATTR_MC_LINK) - # As the musiccast group has changed, we need to trigger the servers ha state. - # In other cases this happens due to the callback after the dist updated message. - server.async_write_ha_state() - return + await self.coordinator.musiccast.zone_join(self._zone_id) + return False _LOGGER.debug("%s will now join as a client", self.entity_id) await self.coordinator.musiccast.mc_client_join( server.ip_address, group_id, self._zone_id ) - - # Ensure that mc link is selected. If main sync was selected previously, it's possible that this does not - # happen automatically - await self.async_select_source(ATTR_MC_LINK) + return True async def async_client_leave_group(self, force=False): """Make self leave the group. @@ -728,18 +742,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): _LOGGER.debug("%s client leave called", self.entity_id) if not force and ( self.source == ATTR_MAIN_SYNC - or len( - [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK] - ) - > 0 + or [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK] ): - # If we are only syncing to main or another zone is also using the musiccast module as client, don't - # kill the client session, just select a dummy source. - save_inputs = self.coordinator.musiccast.get_save_inputs(self._zone_id) - if len(save_inputs): - await self.async_select_source(save_inputs[0]) - # Then turn off the zone - await self.async_turn_off() + await self.coordinator.musiccast.zone_unjoin(self._zone_id) else: servers = [ server @@ -747,14 +752,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): if server.coordinator.data.group_id == self.coordinator.data.group_id ] await self.coordinator.musiccast.mc_client_unjoin() - if len(servers): + if servers: await servers[0].coordinator.musiccast.mc_server_group_reduce( servers[0].zone_id, [self.ip_address], self.get_distribution_num() ) - for server in self.get_all_server_entities(): - await server.async_check_client_list() - # Internal server functions async def async_server_close_group(self): @@ -762,7 +764,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): Should only be called for servers. """ - _LOGGER.info("%s closes his group", self.entity_id) + _LOGGER.debug("%s closes his group", self.entity_id) for client in self.musiccast_group: if client != self: await client.async_client_leave_group() @@ -770,6 +772,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_check_client_list(self): """Let the server check if all its clients are still part of his group.""" + if not self.is_server or self.coordinator.data.group_update_lock.locked(): + return + _LOGGER.debug("%s updates his group members", self.entity_id) client_ips_for_removal = [] for expected_client_ip in self.coordinator.data.group_client_list: @@ -779,8 +784,8 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): # The client is no longer part of the group. Prepare removal. client_ips_for_removal.append(expected_client_ip) - if len(client_ips_for_removal) > 0: - _LOGGER.info( + if client_ips_for_removal: + _LOGGER.debug( "%s says good bye to the following members %s", self.entity_id, str(client_ips_for_removal), @@ -793,3 +798,8 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): await self.async_server_close_group() self.async_write_ha_state() + + @callback + def async_schedule_check_client_list(self): + """Schedule async_check_client_list.""" + self.hass.create_task(self.async_check_client_list()) diff --git a/requirements_all.txt b/requirements_all.txt index c348e03b06a..2256474d3b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -212,7 +212,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.8.0 +aiomusiccast==0.8.2 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c3fe00b068..6d1c0afd6b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.8.0 +aiomusiccast==0.8.2 # homeassistant.components.notion aionotion==3.0.2 From 80aaebb7610ccaafdd65a44bf0aad897baaff304 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Mon, 26 Jul 2021 19:52:30 +0100 Subject: [PATCH 597/818] Rename Prometheus metrics to conform with naming guidelines (#50156) --- .../components/prometheus/__init__.py | 22 ++++++---- tests/components/prometheus/test_init.py | 44 +++++++++++++++---- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index c74caa745f3..f158d2506d1 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -54,12 +54,14 @@ COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema( {vol.Optional(CONF_OVERRIDE_METRIC): cv.string} ) +DEFAULT_NAMESPACE = "homeassistant" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( { vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, - vol.Optional(CONF_PROM_NAMESPACE): cv.string, + vol.Optional(CONF_PROM_NAMESPACE, default=DEFAULT_NAMESPACE): cv.string, vol.Optional(CONF_DEFAULT_METRIC): cv.string, vol.Optional(CONF_OVERRIDE_METRIC): cv.string, vol.Optional(CONF_COMPONENT_CONFIG, default={}): vol.Schema( @@ -291,7 +293,9 @@ class PrometheusMetrics: def _handle_light(self, state): metric = self._metric( - "light_state", self.prometheus_cli.Gauge, "Load level of a light (0..1)" + "light_brightness_percent", + self.prometheus_cli.Gauge, + "Light brightness percentage (0..100)", ) try: @@ -317,9 +321,9 @@ class PrometheusMetrics: if self._climate_units == TEMP_FAHRENHEIT: temp = fahrenheit_to_celsius(temp) metric = self._metric( - "temperature_c", + "climate_target_temperature_celsius", self.prometheus_cli.Gauge, - "Temperature in degrees Celsius", + "Target temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(temp) @@ -328,9 +332,9 @@ class PrometheusMetrics: if self._climate_units == TEMP_FAHRENHEIT: current_temp = fahrenheit_to_celsius(current_temp) metric = self._metric( - "current_temperature_c", + "climate_current_temperature_celsius", self.prometheus_cli.Gauge, - "Current Temperature in degrees Celsius", + "Current temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(current_temp) @@ -414,7 +418,7 @@ class PrometheusMetrics: """Get metric based on device class attribute.""" metric = state.attributes.get(ATTR_DEVICE_CLASS) if metric is not None: - return f"{metric}_{unit}" + return f"sensor_{metric}_{unit}" return None def _sensor_override_metric(self, state, unit): @@ -442,8 +446,8 @@ class PrometheusMetrics: return units = { - TEMP_CELSIUS: "c", - TEMP_FAHRENHEIT: "c", # F should go into C metric + TEMP_CELSIUS: "celsius", + TEMP_FAHRENHEIT: "celsius", # F should go into C metric PERCENTAGE: "percent", } default = unit.replace("/", "_per_") diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index b1abe1de3e5..f8fcdd4561a 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -31,9 +31,12 @@ class FilterTest: should_pass: bool -async def prometheus_client(hass, hass_client): +async def prometheus_client(hass, hass_client, namespace): """Initialize an hass_client with Prometheus component.""" - await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + config = {} + if namespace is not None: + config[prometheus.CONF_PROM_NAMESPACE] = namespace + await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: config}) await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) @@ -98,9 +101,9 @@ async def prometheus_client(hass, hass_client): return await hass_client() -async def test_view(hass, hass_client): +async def test_view_empty_namespace(hass, hass_client): """Test prometheus metrics view.""" - client = await prometheus_client(hass, hass_client) + client = await prometheus_client(hass, hass_client, "") resp = await client.get(prometheus.API_ENDPOINT) assert resp.status == 200 @@ -117,7 +120,7 @@ async def test_view(hass, hass_client): ) assert ( - 'temperature_c{domain="sensor",' + 'sensor_temperature_celsius{domain="sensor",' 'entity="sensor.outside_temperature",' 'friendly_name="Outside Temperature"} 15.6' in body ) @@ -129,7 +132,7 @@ async def test_view(hass, hass_client): ) assert ( - 'current_temperature_c{domain="climate",' + 'climate_current_temperature_celsius{domain="climate",' 'entity="climate.heatpump",' 'friendly_name="HeatPump"} 25.0' in body ) @@ -160,7 +163,7 @@ async def test_view(hass, hass_client): ) assert ( - 'humidity_percent{domain="sensor",' + 'sensor_humidity_percent{domain="sensor",' 'entity="sensor.outside_humidity",' 'friendly_name="Outside Humidity"} 54.0' in body ) @@ -172,7 +175,7 @@ async def test_view(hass, hass_client): ) assert ( - 'power_kwh{domain="sensor",' + 'sensor_power_kwh{domain="sensor",' 'entity="sensor.radio_energy",' 'friendly_name="Radio Energy"} 14.0' in body ) @@ -208,6 +211,31 @@ async def test_view(hass, hass_client): ) +async def test_view_default_namespace(hass, hass_client): + """Test prometheus metrics view.""" + client = await prometheus_client(hass, hass_client, None) + resp = await client.get(prometheus.API_ENDPOINT) + + assert resp.status == 200 + assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN + body = await resp.text() + body = body.split("\n") + + assert len(body) > 3 + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'homeassistant_sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + @pytest.fixture(name="mock_client") def mock_client_fixture(): """Mock the prometheus client.""" From cfb0def71897f2410965c1a6e9d782288db94774 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 26 Jul 2021 21:20:34 +0200 Subject: [PATCH 598/818] Change integration modbus to use _attr variables (#53511) --- .../components/modbus/base_platform.py | 40 +++------ .../components/modbus/binary_sensor.py | 4 +- homeassistant/components/modbus/climate.py | 85 +++++-------------- homeassistant/components/modbus/cover.py | 15 ++-- homeassistant/components/modbus/sensor.py | 11 +-- 5 files changed, 45 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 0b612c3ecf5..b321183fd66 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -60,17 +60,19 @@ class BasePlatform(Entity): def __init__(self, hub: ModbusHub, entry: dict[str, Any]) -> None: """Initialize the Modbus binary sensor.""" self._hub = hub - self._name = entry[CONF_NAME] self._slave = entry.get(CONF_SLAVE) self._address = int(entry[CONF_ADDRESS]) - self._device_class = entry.get(CONF_DEVICE_CLASS) self._input_type = entry[CONF_INPUT_TYPE] self._value = None - self._available = True self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) - self._available = self._scan_interval == 0 self._call_active = False + self._attr_name = entry[CONF_NAME] + self._attr_should_poll = False + self._attr_device_class = entry.get(CONF_DEVICE_CLASS) + self._attr_available = True + self._attr_unit_of_measurement = None + @abstractmethod async def async_update(self, now=None): """Virtual function to be overwritten.""" @@ -82,26 +84,6 @@ class BasePlatform(Entity): self.hass, self.async_update, timedelta(seconds=self._scan_interval) ) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._device_class - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - class BaseStructPlatform(BasePlatform, RestoreEntity): """Base class representing a sensor/climate.""" @@ -229,11 +211,11 @@ class BaseSwitch(BasePlatform, RestoreEntity): self._slave, self._address, command, self._write_type ) if result is None: - self._available = False + self._attr_available = False self.async_write_ha_state() return - self._available = True + self._attr_available = True if not self._verify_active: self._is_on = command == self.command_on self.async_write_ha_state() @@ -253,7 +235,7 @@ class BaseSwitch(BasePlatform, RestoreEntity): # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval if not self._verify_active: - self._available = True + self._attr_available = True self.async_write_ha_state() return @@ -266,11 +248,11 @@ class BaseSwitch(BasePlatform, RestoreEntity): ) self._call_active = False if result is None: - self._available = False + self._attr_available = False self.async_write_ha_state() return - self._available = True + self._attr_available = True if self._verify_type == CALL_TYPE_COIL: self._is_on = bool(result.bits[0] & 1) else: diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 0188210be8a..ac635c76275 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -64,10 +64,10 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): ) self._call_active = False if result is None: - self._available = False + self._attr_available = False self.async_write_ha_state() return self._value = result.bits[0] & 1 - self._available = True + self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index e8f281bdd1f..1353828b926 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -76,76 +76,37 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Initialize the modbus thermostat.""" super().__init__(hub, config) self._target_temperature_register = config[CONF_TARGET_TEMP] - self._target_temperature = None - self._current_temperature = None - self._structure = config[CONF_STRUCTURE] self._unit = config[CONF_TEMPERATURE_UNIT] - self._max_temp = config[CONF_MAX_TEMP] - self._min_temp = config[CONF_MIN_TEMP] - self._temp_step = config[CONF_STEP] + self._structure = config[CONF_STRUCTURE] + + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + self._attr_hvac_mode = HVAC_MODE_AUTO + self._attr_hvac_modes = [HVAC_MODE_AUTO] + self._attr_current_temperature = None + self._attr_target_temperature = None + self._attr_temperature_unit = ( + TEMP_FAHRENHEIT if self._unit == "F" else TEMP_CELSIUS + ) + self._attr_precision = ( + PRECISION_TENTHS if self._precision >= 1 else PRECISION_WHOLE + ) + self._attr_min_temp = config[CONF_MIN_TEMP] + self._attr_max_temp = config[CONF_MAX_TEMP] + self._attr_target_temperature_step = config[CONF_TARGET_TEMP] + self._attr_target_temperature_step = config[CONF_STEP] async def async_added_to_hass(self): """Handle entity which will be added.""" await self.async_base_added_to_hass() state = await self.async_get_last_state() if state and state.attributes.get(ATTR_TEMPERATURE): - self._target_temperature = float(state.attributes[ATTR_TEMPERATURE]) - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE - - @property - def hvac_mode(self): - """Return the current HVAC mode.""" - return HVAC_MODE_AUTO - - @property - def hvac_modes(self): - """Return the possible HVAC modes.""" - return [HVAC_MODE_AUTO] + self._attr_target_temperature = float(state.attributes[ATTR_TEMPERATURE]) async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" # Home Assistant expects this method. # We'll keep it here to avoid getting exceptions. - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the target temperature.""" - return self._target_temperature - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT if self._unit == "F" else TEMP_CELSIUS - - @property - def precision(self) -> float: - """Return the precision of the system.""" - return PRECISION_TENTHS if self._precision >= 1 else PRECISION_WHOLE - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self._min_temp - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self._max_temp - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self._temp_step - async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if ATTR_TEMPERATURE not in kwargs: @@ -174,7 +135,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): registers, CALL_TYPE_WRITE_REGISTERS, ) - self._available = result is not None + self._attr_available = result is not None await self.async_update() async def async_update(self, now=None): @@ -186,10 +147,10 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): if self._call_active: return self._call_active = True - self._target_temperature = await self._async_read_register( + self._attr_target_temperature = await self._async_read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) - self._current_temperature = await self._async_read_register( + self._attr_current_temperature = await self._async_read_register( self._input_type, self._address ) self._call_active = False @@ -201,12 +162,12 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._slave, register, self._count, register_type ) if result is None: - self._available = False + self._attr_available = False return -1 self.unpack_structure_result(result.registers) - self._available = True + self._attr_available = True if self._value is None: return None diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index bd150434dc1..98a352f218a 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -73,6 +73,8 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): self._status_register = config.get(CONF_STATUS_REGISTER) self._status_register_type = config[CONF_STATUS_REGISTER_TYPE] + self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + # If we read cover status from coil, and not from optional status register, # we interpret boolean value False as closed cover, and value True as open cover. # Intermediate states are not supported in such a setup. @@ -109,11 +111,6 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): } self._value = convert[state.state] - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE - @property def is_opening(self): """Return if the cover is opening or not.""" @@ -134,7 +131,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): result = await self._hub.async_pymodbus_call( self._slave, self._write_address, self._state_open, self._write_type ) - self._available = result is not None + self._attr_available = result is not None await self.async_update() async def async_close_cover(self, **kwargs: Any) -> None: @@ -142,7 +139,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): result = await self._hub.async_pymodbus_call( self._slave, self._write_address, self._state_closed, self._write_type ) - self._available = result is not None + self._attr_available = result is not None await self.async_update() async def async_update(self, now=None): @@ -158,10 +155,10 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): ) self._call_active = False if result is None: - self._available = False + self._attr_available = False self.async_write_ha_state() return None - self._available = True + self._attr_available = True if self._input_type == CALL_TYPE_COIL: self._value = bool(result.bits[0] & 1) else: diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 1f2b5e32f6f..e969fa23a65 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -47,7 +47,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) - self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -61,11 +61,6 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): """Return the state of the sensor.""" return self._value - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - async def async_update(self, now=None): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with @@ -74,10 +69,10 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self._slave, self._address, self._count, self._input_type ) if result is None: - self._available = False + self._attr_available = False self.async_write_ha_state() return self.unpack_structure_result(result.registers) - self._available = True + self._attr_available = True self.async_write_ha_state() From aee48dbcb3d38d431254147118b45c4e2de6aed0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 26 Jul 2021 15:23:32 -0400 Subject: [PATCH 599/818] Use entity class attributes for cloud (#53445) --- .../components/cloud/binary_sensor.py | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 0e3b20fa011..f96bda4ce1b 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -24,41 +24,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class CloudRemoteBinary(BinarySensorEntity): """Representation of an Cloud Remote UI Connection binary sensor.""" + _attr_name = "Remote UI" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_should_poll = False + _attr_unique_id = "cloud-remote-ui-connectivity" + def __init__(self, cloud): """Initialize the binary sensor.""" self.cloud = cloud self._unsub_dispatcher = None - @property - def name(self) -> str: - """Return the name of the binary sensor, if any.""" - return "Remote UI" - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return "cloud-remote-ui-connectivity" - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self.cloud.remote.is_connected - @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_CONNECTIVITY - @property def available(self) -> bool: """Return True if entity is available.""" return self.cloud.remote.certificate is not None - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state.""" - return False - async def async_added_to_hass(self): """Register update dispatcher.""" From 88cffc86bb5456dfd374f62ae470f27f8746c996 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 26 Jul 2021 14:58:13 -0500 Subject: [PATCH 600/818] Add crossfade control support to Sonos (#53228) --- homeassistant/components/sonos/media_player.py | 6 ++++++ homeassistant/components/sonos/services.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index ecd22c89a87..0948e971baf 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -120,6 +120,7 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_MASTER = "master" ATTR_WITH_GROUP = "with_group" ATTR_BUTTONS_ENABLED = "buttons_enabled" +ATTR_CROSSFADE = "crossfade" ATTR_NIGHT_SOUND = "night_sound" ATTR_SPEECH_ENHANCE = "speech_enhance" ATTR_QUEUE_POSITION = "queue_position" @@ -231,6 +232,7 @@ async def async_setup_entry( SERVICE_SET_OPTION, { vol.Optional(ATTR_BUTTONS_ENABLED): cv.boolean, + vol.Optional(ATTR_CROSSFADE): cv.boolean, vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, vol.Optional(ATTR_STATUS_LIGHT): cv.boolean, @@ -609,6 +611,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): def set_option( self, buttons_enabled: bool | None = None, + crossfade: bool | None = None, night_sound: bool | None = None, speech_enhance: bool | None = None, status_light: bool | None = None, @@ -617,6 +620,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if buttons_enabled is not None: self.soco.buttons_enabled = buttons_enabled + if crossfade is not None: + self.soco.cross_fade = crossfade + if night_sound is not None and self.speaker.night_mode is not None: self.soco.night_mode = night_sound diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 365bdc29b37..76bc656f990 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -100,6 +100,12 @@ set_option: example: "true" selector: boolean: + crossfade: + name: Crossfade + description: Enable crossfade on the device + example: "true" + selector: + boolean: night_sound: name: Night sound description: Enable Night Sound mode From ee452d415d2c20a793a4ab43cd2a36a0b6f0d4fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Jul 2021 22:00:43 +0200 Subject: [PATCH 601/818] Add SensorEntityDescription class (#53357) --- homeassistant/components/ambee/const.py | 381 ++++++++++-------- homeassistant/components/ambee/models.py | 15 - homeassistant/components/ambee/sensor.py | 45 +-- homeassistant/components/sensor/__init__.py | 28 +- .../components/template/template_entity.py | 4 +- homeassistant/helpers/entity.py | 67 ++- 6 files changed, 293 insertions(+), 247 deletions(-) delete mode 100644 homeassistant/components/ambee/models.py diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 730c6780f4f..d2570bea710 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -5,12 +5,11 @@ from datetime import timedelta import logging from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -18,13 +17,10 @@ from homeassistant.const import ( DEVICE_CLASS_CO, ) -from .models import AmbeeSensor - DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=1) -ATTR_ENABLED_BY_DEFAULT: Final = "enabled_by_default" ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" @@ -38,175 +34,202 @@ SERVICES: dict[str, str] = { SERVICE_POLLEN: "Pollen", } -SENSORS: dict[str, dict[str, AmbeeSensor]] = { - SERVICE_AIR_QUALITY: { - "particulate_matter_2_5": { - ATTR_NAME: "Particulate Matter < 2.5 μm", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "particulate_matter_10": { - ATTR_NAME: "Particulate Matter < 10 μm", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "sulphur_dioxide": { - ATTR_NAME: "Sulphur Dioxide (SO2)", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "nitrogen_dioxide": { - ATTR_NAME: "Nitrogen Dioxide (NO2)", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "ozone": { - ATTR_NAME: "Ozone", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "carbon_monoxide": { - ATTR_NAME: "Carbon Monoxide (CO)", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "air_quality_index": { - ATTR_NAME: "Air Quality Index (AQI)", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - }, - SERVICE_POLLEN: { - "grass": { - ATTR_NAME: "Grass Pollen", - ATTR_ICON: "mdi:grass", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - }, - "tree": { - ATTR_NAME: "Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - }, - "weed": { - ATTR_NAME: "Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - }, - "grass_risk": { - ATTR_NAME: "Grass Pollen Risk", - ATTR_ICON: "mdi:grass", - ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, - }, - "tree_risk": { - ATTR_NAME: "Tree Pollen Risk", - ATTR_ICON: "mdi:tree", - ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, - }, - "weed_risk": { - ATTR_NAME: "Weed Pollen Risk", - ATTR_ICON: "mdi:sprout", - ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, - }, - "grass_poaceae": { - ATTR_NAME: "Poaceae Grass Pollen", - ATTR_ICON: "mdi:grass", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_alder": { - ATTR_NAME: "Alder Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_birch": { - ATTR_NAME: "Birch Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_cypress": { - ATTR_NAME: "Cypress Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_elm": { - ATTR_NAME: "Elm Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_hazel": { - ATTR_NAME: "Hazel Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_oak": { - ATTR_NAME: "Oak Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_pine": { - ATTR_NAME: "Pine Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_plane": { - ATTR_NAME: "Plane Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_poplar": { - ATTR_NAME: "Poplar Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_chenopod": { - ATTR_NAME: "Chenopod Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_mugwort": { - ATTR_NAME: "Mugwort Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_nettle": { - ATTR_NAME: "Nettle Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_ragweed": { - ATTR_NAME: "Ragweed Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - }, +SENSORS: dict[str, list[SensorEntityDescription]] = { + SERVICE_AIR_QUALITY: [ + SensorEntityDescription( + key="particulate_matter_2_5", + name="Particulate Matter < 2.5 μm", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="particulate_matter_10", + name="Particulate Matter < 10 μm", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="sulphur_dioxide", + name="Sulphur Dioxide (SO2)", + unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="nitrogen_dioxide", + name="Nitrogen Dioxide (NO2)", + unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="ozone", + name="Ozone", + unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="carbon_monoxide", + name="Carbon Monoxide (CO)", + device_class=DEVICE_CLASS_CO, + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="air_quality_index", + name="Air Quality Index (AQI)", + state_class=STATE_CLASS_MEASUREMENT, + ), + ], + SERVICE_POLLEN: [ + SensorEntityDescription( + key="grass", + name="Grass Pollen", + icon="mdi:grass", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="tree", + name="Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="weed", + name="Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="grass_risk", + name="Grass Pollen Risk", + icon="mdi:grass", + device_class=DEVICE_CLASS_AMBEE_RISK, + ), + SensorEntityDescription( + key="tree_risk", + name="Tree Pollen Risk", + icon="mdi:tree", + device_class=DEVICE_CLASS_AMBEE_RISK, + ), + SensorEntityDescription( + key="weed_risk", + name="Weed Pollen Risk", + icon="mdi:sprout", + device_class=DEVICE_CLASS_AMBEE_RISK, + ), + SensorEntityDescription( + key="grass_poaceae", + name="Poaceae Grass Pollen", + icon="mdi:grass", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_alder", + name="Alder Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_birch", + name="Birch Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_cypress", + name="Cypress Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_elm", + name="Elm Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_hazel", + name="Hazel Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_oak", + name="Oak Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_pine", + name="Pine Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_plane", + name="Plane Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_poplar", + name="Poplar Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_chenopod", + name="Chenopod Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_mugwort", + name="Mugwort Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_nettle", + name="Nettle Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_ragweed", + name="Ragweed Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + ], } diff --git a/homeassistant/components/ambee/models.py b/homeassistant/components/ambee/models.py deleted file mode 100644 index 871aeed332b..00000000000 --- a/homeassistant/components/ambee/models.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Models helper class for the Ambee integration.""" -from __future__ import annotations - -from typing import TypedDict - - -class AmbeeSensor(TypedDict, total=False): - """Represent an Ambee Sensor.""" - - device_class: str - enabled_by_default: bool - icon: str - name: str - state_class: str - unit_of_measurement: str diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index 54e67160822..ecd04ffd204 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -2,19 +2,12 @@ from __future__ import annotations from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, -) +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -23,15 +16,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ( - ATTR_ENABLED_BY_DEFAULT, - ATTR_ENTRY_TYPE, - DOMAIN, - ENTRY_TYPE_SERVICE, - SENSORS, - SERVICES, -) -from .models import AmbeeSensor +from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES async def async_setup_entry( @@ -44,13 +29,12 @@ async def async_setup_entry( AmbeeSensorEntity( coordinator=hass.data[DOMAIN][entry.entry_id][service_key], entry_id=entry.entry_id, - sensor_key=sensor_key, - sensor=sensor, + description=description, service_key=service_key, service=SERVICES[service_key], ) for service_key, service_sensors in SENSORS.items() - for sensor_key, sensor in service_sensors.items() + for description in service_sensors ) @@ -62,26 +46,17 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): *, coordinator: DataUpdateCoordinator, entry_id: str, - sensor_key: str, - sensor: AmbeeSensor, + description: SensorEntityDescription, service_key: str, service: str, ) -> None: """Initialize Ambee sensor.""" super().__init__(coordinator=coordinator) - self._sensor_key = sensor_key self._service_key = service_key - self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{sensor_key}" - self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) - self._attr_entity_registry_enabled_default = sensor.get( - ATTR_ENABLED_BY_DEFAULT, True - ) - self._attr_icon = sensor.get(ATTR_ICON) - self._attr_name = sensor.get(ATTR_NAME) - self._attr_state_class = sensor.get(ATTR_STATE_CLASS) - self._attr_unique_id = f"{entry_id}_{service_key}_{sensor_key}" - self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) + self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{description.key}" + self.entity_description = description + self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" self._attr_device_info = { ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")}, @@ -93,7 +68,7 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): @property def state(self) -> StateType: """Return the state of the sensor.""" - value = getattr(self.coordinator.data, self._sensor_key) + value = getattr(self.coordinator.data, self.entity_description.key) if isinstance(value, str): return value.lower() return value # type: ignore[no-any-return] diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 8fac4e50b3e..59bda470c63 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any, Final, cast, final @@ -31,7 +32,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -95,21 +96,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class SensorEntityDescription(EntityDescription): + """An class that describes sensor entities.""" + + state_class: str | None = None + last_reset: datetime | None = None + + class SensorEntity(Entity): """Base class for sensor entities.""" - _attr_state_class: str | None = None - _attr_last_reset: datetime | None = None + entity_description: SensorEntityDescription + _attr_state_class: str | None + _attr_last_reset: datetime | None @property def state_class(self) -> str | None: """Return the state class of this entity, from STATE_CLASSES, if any.""" - return self._attr_state_class + if hasattr(self, "_attr_state_class"): + return self._attr_state_class + if hasattr(self, "entity_description"): + return self.entity_description.state_class + return None @property def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if any.""" - return self._attr_last_reset + if hasattr(self, "_attr_last_reset"): + return self._attr_last_reset + if hasattr(self, "entity_description"): + return self.entity_description.last_reset + return None @property def capability_attributes(self) -> Mapping[str, Any] | None: diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 7bf6d6109be..6bf889ebf02 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -112,6 +112,9 @@ class _TemplateAttribute: class TemplateEntity(Entity): """Entity that uses templates to calculate attributes.""" + _attr_available = True + _attr_entity_picture = None + _attr_icon = None _attr_should_poll = False def __init__( @@ -128,7 +131,6 @@ class TemplateEntity(Entity): self._attribute_templates = attribute_templates self._attr_extra_state_attributes = {} self._availability_template = availability_template - self._attr_available = True self._icon_template = icon_template self._entity_picture_template = entity_picture_template self._self_ref_update_count = 0 diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a50afd410e9..e66624cd15a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC import asyncio from collections.abc import Awaitable, Iterable, Mapping, MutableMapping +from dataclasses import dataclass from datetime import datetime, timedelta import functools as ft import logging @@ -178,6 +179,21 @@ class DeviceInfo(TypedDict, total=False): default_model: str +@dataclass +class EntityDescription: + """An class that describes Home Assistant entities.""" + + # This is the key identifier for this entity + key: str + + device_class: str | None = None + entity_registry_enabled_default: bool = True + force_update: bool = False + icon: str | None = None + name: str | None = None + unit_of_measurement: str | None = None + + class Entity(ABC): """An abstract class for Home Assistant entities.""" @@ -194,6 +210,9 @@ class Entity(ABC): # Owning platform instance. Will be set by EntityPlatform platform: EntityPlatform | None = None + # Entity description instance for this Entity + entity_description: EntityDescription + # If we reported if this entity was slow _slow_reported = False @@ -223,19 +242,19 @@ class Entity(ABC): _attr_assumed_state: bool = False _attr_available: bool = True _attr_context_recent_time: timedelta = timedelta(seconds=5) - _attr_device_class: str | None = None + _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None _attr_entity_picture: str | None = None - _attr_entity_registry_enabled_default: bool = True + _attr_entity_registry_enabled_default: bool _attr_extra_state_attributes: MutableMapping[str, Any] | None = None - _attr_force_update: bool = False - _attr_icon: str | None = None - _attr_name: str | None = None + _attr_force_update: bool + _attr_icon: str | None + _attr_name: str | None _attr_should_poll: bool = True _attr_state: StateType = STATE_UNKNOWN _attr_supported_features: int | None = None _attr_unique_id: str | None = None - _attr_unit_of_measurement: str | None = None + _attr_unit_of_measurement: str | None @property def should_poll(self) -> bool: @@ -253,7 +272,11 @@ class Entity(ABC): @property def name(self) -> str | None: """Return the name of the entity.""" - return self._attr_name + if hasattr(self, "_attr_name"): + return self._attr_name + if hasattr(self, "entity_description"): + return self.entity_description.name + return None @property def state(self) -> StateType: @@ -309,17 +332,29 @@ class Entity(ABC): @property def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - return self._attr_device_class + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return self._attr_unit_of_measurement + if hasattr(self, "_attr_unit_of_measurement"): + return self._attr_unit_of_measurement + if hasattr(self, "entity_description"): + return self.entity_description.unit_of_measurement + return None @property def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" - return self._attr_icon + if hasattr(self, "_attr_icon"): + return self._attr_icon + if hasattr(self, "entity_description"): + return self.entity_description.icon + return None @property def entity_picture(self) -> str | None: @@ -343,7 +378,11 @@ class Entity(ABC): If True, a state change will be triggered anytime the state property is updated, not just when the value changes. """ - return self._attr_force_update + if hasattr(self, "_attr_force_update"): + return self._attr_force_update + if hasattr(self, "entity_description"): + return self.entity_description.force_update + return False @property def supported_features(self) -> int | None: @@ -358,7 +397,11 @@ class Entity(ABC): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._attr_entity_registry_enabled_default + if hasattr(self, "_attr_entity_registry_enabled_default"): + return self._attr_entity_registry_enabled_default + if hasattr(self, "entity_description"): + return self.entity_description.entity_registry_enabled_default + return True # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they From 1681bbe5a55a8847eed8053fba4056a33ba1891a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 26 Jul 2021 22:11:27 +0100 Subject: [PATCH 602/818] Make sure that vocolinc flowerbud humidity sensor is exposed (via homekit_controller) (#53518) * Make sure that vocolinc flowerbud humidity sensor is exposed * Was a no-op to request these from humidifier, so remove them * Fix typo --- .../components/homekit_controller/const.py | 3 +++ .../homekit_controller/humidifier.py | 2 -- .../components/homekit_controller/sensor.py | 10 +++++++++ .../test_vocolinc_flowerbud.py | 22 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 321113cf8df..5b4c87f53e4 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -49,4 +49,7 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: "sensor", CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: "number", CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): "sensor", + CharacteristicsTypes.get_uuid( + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT + ): "sensor", } diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index dfddd29f2ff..1505ead993b 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -41,7 +41,6 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, - CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, @@ -143,7 +142,6 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, - CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index c24f46198c0..f34fc3f0a9b 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -53,6 +53,16 @@ SIMPLE_SENSOR = { "probe": lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), }, + CharacteristicsTypes.get_uuid(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT): { + "name": "Current Humidity", + "device_class": DEVICE_CLASS_HUMIDITY, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": PERCENTAGE, + # This sensor is only for humidity characteristics that are not part + # of a humidity sensor service. + "probe": lambda char: char.service.type + != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR), + }, } diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index ee4713b012b..e2762b5c153 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -68,3 +68,25 @@ async def test_vocolinc_flowerbud_setup(hass): # The sensor and switch should be part of the same device assert entry.device_id == device.id + + # Assert the humidity sensory is detected + entry = entity_registry.async_get( + "sensor.vocolinc_flowerbud_0d324b_current_humidity" + ) + assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:30" + + helper = Helper( + hass, + "sensor.vocolinc_flowerbud_0d324b_current_humidity", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert ( + state.attributes["friendly_name"] + == "VOCOlinc-Flowerbud-0d324b - Current Humidity" + ) + + # The sensor and humidifier should be part of the same device + assert entry.device_id == device.id From c1c6c54b45cbf8cff6e6e556333cc728da9468d3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 26 Jul 2021 23:18:20 +0200 Subject: [PATCH 603/818] xknx 0.18.9 (#53519) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index e075411d8b8..c514ec6fe64 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.8"], + "requirements": ["xknx==0.18.9"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2256474d3b6..df0e333deac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2397,7 +2397,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.8 +xknx==0.18.9 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d1c0afd6b2..13d25c3867c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1315,7 +1315,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.8 +xknx==0.18.9 # homeassistant.components.bluesound # homeassistant.components.fritz From 2127314f9eeae7cf2fc620a00a645dd24d368084 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 26 Jul 2021 23:33:15 +0200 Subject: [PATCH 604/818] Fix typo in codeowners (#53520) --- CODEOWNERS | 4 ++-- homeassistant/components/coronavirus/manifest.json | 2 +- homeassistant/components/energy/manifest.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f872400c857..9562817b27f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -99,7 +99,7 @@ homeassistant/components/configurator/* @home-assistant/core homeassistant/components/control4/* @lawtancool homeassistant/components/conversation/* @home-assistant/core homeassistant/components/coolmaster/* @OnFreund -homeassistant/components/coronavirus/* @home_assistant/core +homeassistant/components/coronavirus/* @home-assistant/core homeassistant/components/counter/* @fabaff homeassistant/components/cover/* @home-assistant/core homeassistant/components/cpuspeed/* @fabaff @@ -141,7 +141,7 @@ homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin homeassistant/components/emonitor/* @bdraco homeassistant/components/emulated_kasa/* @kbickar -homeassistant/components/energy/* @home_assistant/core +homeassistant/components/energy/* @home-assistant/core homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/enphase_envoy/* @gtdiehl diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json index 08a88d1b826..87410d8b572 100644 --- a/homeassistant/components/coronavirus/manifest.json +++ b/homeassistant/components/coronavirus/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coronavirus", "requirements": ["coronavirus==1.1.1"], - "codeowners": ["@home_assistant/core"], + "codeowners": ["@home-assistant/core"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index ee6fd784823..3a3cbeff4e7 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -2,7 +2,7 @@ "domain": "energy", "name": "Energy", "documentation": "https://www.home-assistant.io/integrations/energy", - "codeowners": ["@home_assistant/core"], + "codeowners": ["@home-assistant/core"], "iot_class": "calculated", "dependencies": ["websocket_api", "history"], "quality_scale": "internal" From 7cb3414517830a7bb9862d3eff92bfa5b460e480 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 26 Jul 2021 23:56:40 +0200 Subject: [PATCH 605/818] Update frontend to 20210726.0 (#53522) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7af6e2bc733..83e6d71712a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210707.0" + "home-assistant-frontend==20210726.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19682407737..37cfbe539aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210707.0 +home-assistant-frontend==20210726.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index df0e333deac..c5f73ca53cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -785,7 +785,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210707.0 +home-assistant-frontend==20210726.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13d25c3867c..2b327269c47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,7 +455,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210707.0 +home-assistant-frontend==20210726.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0b442652322edc8f704ff0409dcce14735f0f9db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 27 Jul 2021 00:22:21 +0200 Subject: [PATCH 606/818] Add description classes to entity components (#53521) * Add description classes to entity components * An -> A * Add StateVacuumEntityDescription --- .../alarm_control_panel/__init__.py | 9 +++++++- .../components/binary_sensor/__init__.py | 9 +++++++- homeassistant/components/climate/__init__.py | 9 +++++++- homeassistant/components/cover/__init__.py | 9 +++++++- homeassistant/components/fan/__init__.py | 10 ++++++++- .../components/humidifier/__init__.py | 9 +++++++- homeassistant/components/light/__init__.py | 8 ++++++- homeassistant/components/lock/__init__.py | 9 +++++++- .../components/media_player/__init__.py | 9 +++++++- homeassistant/components/number/__init__.py | 9 +++++++- homeassistant/components/remote/__init__.py | 9 +++++++- homeassistant/components/select/__init__.py | 9 +++++++- homeassistant/components/sensor/__init__.py | 2 +- homeassistant/components/siren/__init__.py | 9 +++++++- homeassistant/components/switch/__init__.py | 9 +++++++- homeassistant/components/vacuum/__init__.py | 22 ++++++++++++++++++- .../components/water_heater/__init__.py | 9 +++++++- homeassistant/components/weather/__init__.py | 9 +++++++- homeassistant/helpers/entity.py | 8 ++++++- 19 files changed, 157 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 1032150649e..50317e97f2b 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,6 +1,7 @@ """Component to interface with an alarm control panel.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, Final, final @@ -22,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -117,9 +118,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class AlarmControlPanelEntityDescription(EntityDescription): + """A class that describes alarm control panel entities.""" + + class AlarmControlPanelEntity(Entity): """An abstract class for alarm control entities.""" + entity_description: AlarmControlPanelEntityDescription _attr_changed_by: str | None = None _attr_code_arm_required: bool = True _attr_code_format: str | None = None diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ff97e9af601..2bd5de34d51 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -1,6 +1,7 @@ """Component to interface with binary sensors.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, final @@ -14,7 +15,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType @@ -149,9 +150,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class BinarySensorEntityDescription(EntityDescription): + """A class that describes binary sensor entities.""" + + class BinarySensorEntity(Entity): """Represent a binary sensor.""" + entity_description: BinarySensorEntityDescription _attr_is_on: bool | None = None _attr_state: None = None diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index cc26bcc9bcc..6a46c1986b8 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,6 +1,7 @@ """Provides functionality to interact with climate devices.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import functools as ft import logging @@ -26,7 +27,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, make_entity_service_schema, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType @@ -169,9 +170,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class ClimateEntityDescription(EntityDescription): + """A class that describes climate entities.""" + + class ClimateEntity(Entity): """Base class for climate entities.""" + entity_description: ClimateEntityDescription _attr_current_humidity: int | None = None _attr_current_temperature: float | None = None _attr_fan_mode: str | None diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 110dd09098e..00fef5c6485 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -1,6 +1,7 @@ """Support for Cover devices.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import functools as ft import logging @@ -30,7 +31,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass @@ -170,9 +171,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class CoverEntityDescription(EntityDescription): + """A class that describes cover entities.""" + + class CoverEntity(Entity): """Base class for cover entities.""" + entity_description: CoverEntityDescription _attr_current_cover_position: int | None = None _attr_current_cover_tilt_position: int | None = None _attr_is_closed: bool | None diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 248e7d095d0..1d0caa3231b 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -1,6 +1,7 @@ """Provides functionality to interact with fans.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import functools as ft import logging @@ -22,7 +23,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass from homeassistant.util.percentage import ( @@ -224,9 +225,16 @@ def _fan_native(method): return method +@dataclass +class FanEntityDescription(ToggleEntityDescription): + """A class that describes fan entities.""" + + class FanEntity(ToggleEntity): """Base class for fan entities.""" + entity_description: FanEntityDescription + @_fan_native def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 7839eeec799..d15f70f181a 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -1,6 +1,7 @@ """Provides functionality to interact with humidifier devices.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, final @@ -21,7 +22,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -101,9 +102,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class HumidifierEntityDescription(ToggleEntityDescription): + """A class that describes humidifier entities.""" + + class HumidifierEntity(ToggleEntity): """Base class for humidifier entities.""" + entity_description: HumidifierEntityDescription _attr_available_modes: list[str] | None _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e92999f4d21..68e30b39a61 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, make_entity_service_schema, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -638,9 +638,15 @@ class Profiles: params.setdefault(ATTR_TRANSITION, profile.transition) +@dataclasses.dataclass +class LightEntityDescription(ToggleEntityDescription): + """A class that describes binary sensor entities.""" + + class LightEntity(ToggleEntity): """Base class for light entities.""" + entity_description: LightEntityDescription _attr_brightness: int | None = None _attr_color_mode: str | None = None _attr_color_temp: int | None = None diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index e98202a1ee5..860778d61f3 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -1,6 +1,7 @@ """Component to interface with locks that can be controlled remotely.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import functools as ft import logging @@ -28,7 +29,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, make_entity_service_schema, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType @@ -84,9 +85,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class LockEntityDescription(EntityDescription): + """A class that describes lock entities.""" + + class LockEntity(Entity): """Base class for lock entities.""" + entity_description: LockEntityDescription _attr_changed_by: str | None = None _attr_code_format: str | None = None _attr_is_locked: bool | None = None diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index ffdabe6fed7..6978b90c897 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -5,6 +5,7 @@ import asyncio import base64 import collections from contextlib import suppress +from dataclasses import dataclass import datetime as dt import functools as ft import hashlib @@ -61,7 +62,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, datetime, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass @@ -371,9 +372,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class MediaPlayerEntityDescription(EntityDescription): + """A class that describes media player entities.""" + + class MediaPlayerEntity(Entity): """ABC for media player entities.""" + entity_description: MediaPlayerEntityDescription _access_token: str | None = None _attr_app_id: str | None = None diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index cbfdea7fa11..88ba5cf8b41 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,6 +1,7 @@ """Component to allow numeric input for platforms.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, final @@ -13,7 +14,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -66,9 +67,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class NumberEntityDescription(EntityDescription): + """A class that describes number entities.""" + + class NumberEntity(Entity): """Representation of a Number entity.""" + entity_description: NumberEntityDescription _attr_max_value: float = DEFAULT_MAX_VALUE _attr_min_value: float = DEFAULT_MIN_VALUE _attr_state: None = None diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 0fc4255615e..e67a4eda9a9 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Iterable +from dataclasses import dataclass from datetime import timedelta import functools as ft import logging @@ -24,7 +25,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, make_entity_service_schema, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -142,9 +143,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) +@dataclass +class RemoteEntityDescription(ToggleEntityDescription): + """A class that describes remote entities.""" + + class RemoteEntity(ToggleEntity): """Base class for remote entities.""" + entity_description: RemoteEntityDescription _attr_activity_list: list[str] | None = None _attr_current_activity: str | None = None _attr_supported_features: int = 0 diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 4ec8c46ef05..d5c70c76cd0 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -1,6 +1,7 @@ """Component to allow selecting an option from a list as platforms.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, final @@ -14,7 +15,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -57,9 +58,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class SelectEntityDescription(EntityDescription): + """A class that describes select entities.""" + + class SelectEntity(Entity): """Representation of a Select entity.""" + entity_description: SelectEntityDescription _attr_current_option: str | None _attr_options: list[str] _attr_state: None = None diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 59bda470c63..e36640b1c1d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -98,7 +98,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @dataclass class SensorEntityDescription(EntityDescription): - """An class that describes sensor entities.""" + """A class that describes sensor entities.""" state_class: str | None = None last_reset: datetime | None = None diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 68770daf835..565e8ca0b7a 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -1,6 +1,7 @@ """Component to interface with various sirens/chimes.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, TypedDict, cast, final @@ -15,7 +16,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -121,9 +122,15 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return await component.async_unload_entry(entry) +@dataclass +class SirenEntityDescription(ToggleEntityDescription): + """A class that describes siren entities.""" + + class SirenEntity(ToggleEntity): """Representation of a siren device.""" + entity_description: SirenEntityDescription _attr_available_tones: list[int | str] | None = None @final diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 1ef48fed620..0abbc6d9f97 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -1,6 +1,7 @@ """Component to interface with switches that can be controlled remotely.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, final @@ -19,7 +20,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -84,9 +85,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class SwitchEntityDescription(ToggleEntityDescription): + """A class that describes switch entities.""" + + class SwitchEntity(ToggleEntity): """Base class for switch entities.""" + entity_description: SwitchEntityDescription _attr_current_power_w: float | None = None _attr_today_energy_kwh: float | None = None diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 36cc632d932..87d8d6f49f8 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -1,4 +1,5 @@ """Support for vacuum cleaner robots (botvacs).""" +from dataclasses import dataclass from datetime import timedelta from functools import partial import logging @@ -24,7 +25,12 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, make_entity_service_schema, ) -from homeassistant.helpers.entity import Entity, ToggleEntity +from homeassistant.helpers.entity import ( + Entity, + EntityDescription, + ToggleEntity, + ToggleEntityDescription, +) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.loader import bind_hass @@ -258,9 +264,16 @@ class _BaseVacuum(Entity): ) +@dataclass +class VacuumEntityDescription(ToggleEntityDescription): + """A class that describes vacuum entities.""" + + class VacuumEntity(_BaseVacuum, ToggleEntity): """Representation of a vacuum cleaner robot.""" + entity_description: VacuumEntityDescription + @property def status(self): """Return the status of the vacuum cleaner.""" @@ -338,9 +351,16 @@ class VacuumDevice(VacuumEntity): ) +@dataclass +class StateVacuumEntityDescription(EntityDescription): + """A class that describes vacuum entities.""" + + class StateVacuumEntity(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" + entity_description: StateVacuumEntityDescription + @property def state(self): """Return the state of the vacuum cleaner.""" diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index fcee7a446e0..85d6a791d7f 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,6 +1,7 @@ """Support for water heater devices.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import functools as ft import logging @@ -27,7 +28,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.util.temperature import convert as convert_temperature @@ -135,9 +136,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class WaterHeaterEntityEntityDescription(EntityDescription): + """A class that describes water heater entities.""" + + class WaterHeaterEntity(Entity): """Base class for water heater entities.""" + entity_description: WaterHeaterEntityEntityDescription _attr_current_operation: str | None = None _attr_current_temperature: float | None = None _attr_is_away_mode_on: bool | None = None diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b737129e69e..7d5f7a99d40 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,6 +1,7 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import Final, TypedDict, final @@ -12,7 +13,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp @@ -97,9 +98,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class WeatherEntityDescription(EntityDescription): + """A class that describes weather entities.""" + + class WeatherEntity(Entity): """ABC for weather data.""" + entity_description: WeatherEntityDescription _attr_attribution: str | None = None _attr_condition: str | None _attr_forecast: list[Forecast] | None = None diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e66624cd15a..8c134226f69 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -181,7 +181,7 @@ class DeviceInfo(TypedDict, total=False): @dataclass class EntityDescription: - """An class that describes Home Assistant entities.""" + """A class that describes Home Assistant entities.""" # This is the key identifier for this entity key: str @@ -857,9 +857,15 @@ class Entity(ABC): self.parallel_updates.release() +@dataclass +class ToggleEntityDescription(EntityDescription): + """A class that describes toggle entities.""" + + class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" + entity_description: ToggleEntityDescription _attr_is_on: bool _attr_state: None = None From 31079a05b3daf1c006a0f9aad992fb9bbf87bdc2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 27 Jul 2021 00:42:36 +0200 Subject: [PATCH 607/818] Address late review comments on Netatmo (#53524) --- homeassistant/components/netatmo/camera.py | 5 +---- homeassistant/components/netatmo/const.py | 3 +-- homeassistant/components/netatmo/device_trigger.py | 5 +---- homeassistant/components/netatmo/light.py | 3 +-- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 4ae40181a93..346f9e93647 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -36,7 +36,6 @@ from .const import ( SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, - UNKNOWN, WEBHOOK_LIGHT_MODE, WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_PUSH_TYPE, @@ -132,9 +131,7 @@ class NetatmoCamera(NetatmoBase, Camera): self._id = camera_id self._home_id = home_id - self._device_name = self._data.get_camera(camera_id=camera_id).get( - "name", UNKNOWN - ) + self._device_name = self._data.get_camera(camera_id=camera_id)["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model = camera_type self._attr_unique_id = f"{self._id}-{self._model}" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index f6806ace324..ea0f486b6cc 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -6,7 +6,6 @@ from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN API = "api" -UNKNOWN = "unknown" DOMAIN = "netatmo" MANUFACTURER = "Netatmo" @@ -77,7 +76,7 @@ DATA_SCHEDULES = "netatmo_schedules" NETATMO_WEBHOOK_URL = None NETATMO_EVENT = "netatmo_event" -DEFAULT_PERSON = UNKNOWN +DEFAULT_PERSON = "unknown" DEFAULT_DISCOVERY = True DEFAULT_WEBHOOKS = False diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 65bc79ee712..1bfc736d581 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -144,10 +144,7 @@ async def async_attach_trigger( ATTR_DEVICE_ID: config[ATTR_DEVICE_ID], }, } - # if config[CONF_TYPE] in SUBTYPES: - # event_config[event_trigger.CONF_EVENT_DATA]["data"] = { - # "mode": config[CONF_SUBTYPE] - # } + if config[CONF_TYPE] in SUBTYPES: event_config.update( {event_trigger.CONF_EVENT_DATA: {"data": {"mode": config[CONF_SUBTYPE]}}} diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 717aace1aa2..6fe5e84e65a 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -19,7 +19,6 @@ from .const import ( EVENT_TYPE_LIGHT_MODE, MANUFACTURER, SIGNAL_NAME, - UNKNOWN, WEBHOOK_LIGHT_MODE, WEBHOOK_PUSH_TYPE, ) @@ -89,7 +88,7 @@ class NetatmoLight(NetatmoBase, LightEntity): self._id = camera_id self._home_id = home_id self._model = camera_type - self._device_name: str = self._data.get_camera(camera_id).get("name", UNKNOWN) + self._device_name: str = self._data.get_camera(camera_id)["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" self._is_on = False self._attr_unique_id = f"{self._id}-light" From b26e65d7e5e35a669dc84e089b2ba0c22db4b69b Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 00:47:06 +0200 Subject: [PATCH 608/818] Fix Rituals Perfume Genie sensors icons (#53517) --- .../components/rituals_perfume_genie/sensor.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index d4e10ba141b..7c957722384 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -51,9 +51,12 @@ class DiffuserPerfumeSensor(DiffuserEntity): """Initialize the perfume sensor.""" super().__init__(diffuser, coordinator, PERFUME_SUFFIX) - self._attr_icon = "mdi:tag-remove" - if diffuser.has_cartridge: - self._attr_icon = "mdi:tag-text" + @property + def icon(self) -> str: + """Return the perfume sensor icon.""" + if self._diffuser.has_cartridge: + return "mdi:tag-text" + return "mdi:tag-remove" @property def state(self) -> str: @@ -70,9 +73,12 @@ class DiffuserFillSensor(DiffuserEntity): """Initialize the fill sensor.""" super().__init__(diffuser, coordinator, FILL_SUFFIX) - self._attr_icon = "mdi:beaker-question" - if diffuser.has_cartridge: - self.attr_icon = "mdi:beaker" + @property + def icon(self) -> str: + """Return the fill sensor icon.""" + if self._diffuser.has_cartridge: + return "mdi:beaker" + return "mdi:beaker-question" @property def state(self) -> str: From 8ff3b2a2f604b8233096f1ec32e71accdfd043e0 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 00:49:55 +0200 Subject: [PATCH 609/818] Upgrade pyrituals to 0.0.6 (#53527) --- homeassistant/components/rituals_perfume_genie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 6c46649f688..9ca7556133d 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,7 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": ["pyrituals==0.0.5"], + "requirements": ["pyrituals==0.0.6"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c5f73ca53cc..a265465a05f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1719,7 +1719,7 @@ pyrepetier==3.0.5 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.5 +pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b327269c47..af47d7d196f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -985,7 +985,7 @@ pyqwikswitch==0.93 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.5 +pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.12 From d4c426373056f3ecf7e7df9bf1a5f9a93b8c25e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 27 Jul 2021 01:25:22 +0200 Subject: [PATCH 610/818] Adjust typing of _attr_extra_state_attributes (#53529) --- homeassistant/components/airvisual/__init__.py | 6 ++---- homeassistant/components/guardian/__init__.py | 8 +++----- homeassistant/components/netatmo/netatmo_entity_base.py | 4 +--- homeassistant/components/openuv/__init__.py | 5 +---- homeassistant/components/sia/sia_entity_base.py | 2 +- homeassistant/helpers/entity.py | 6 ++++-- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 015c913b815..21d4054f6ee 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,7 +1,7 @@ """The airvisual component.""" from __future__ import annotations -from collections.abc import Mapping, MutableMapping +from collections.abc import Mapping from datetime import timedelta from math import ceil from typing import Any, Dict, cast @@ -364,9 +364,7 @@ class AirVisualEntity(CoordinatorEntity): """Initialize.""" super().__init__(coordinator) - self._attr_extra_state_attributes: MutableMapping[str, Any] = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION - } + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 96f5ed36720..9338f9a47a9 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, MutableMapping -from typing import Any, cast +from collections.abc import Awaitable +from typing import cast from aioguardian import Client @@ -221,9 +221,7 @@ class GuardianEntity(CoordinatorEntity): """Initialize.""" self._attr_device_class = device_class self._attr_device_info = {"manufacturer": "Elexa"} - self._attr_extra_state_attributes: MutableMapping[str, Any] = { - ATTR_ATTRIBUTION: "Data provided by Elexa" - } + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: "Data provided by Elexa"} self._attr_icon = icon self._attr_name = name self._entry = entry diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 51fc14f6f8e..f276fb3d947 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -30,9 +30,7 @@ class NetatmoBase(Entity): self._model: str = "" self._attr_name = None self._attr_unique_id = None - self._attr_extra_state_attributes: dict = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION - } + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} async def async_added_to_hass(self) -> None: """Entity created.""" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 0de97e52cbe..efe6fa89ca8 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import MutableMapping from typing import Any from pyopenuv import Client @@ -169,9 +168,7 @@ class OpenUvEntity(Entity): def __init__(self, openuv: OpenUV, sensor_type: str) -> None: """Initialize.""" - self._attr_extra_state_attributes: MutableMapping[str, Any] = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION - } + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attr_should_poll = False self._attr_unique_id = ( f"{openuv.client.latitude}_{openuv.client.longitude}_{sensor_type}" diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index 5169702e67b..0a84615d6eb 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -43,7 +43,7 @@ class SIABaseEntity(RestoreEntity): self._cancel_availability_cb: CALLBACK_TYPE | None = None - self._attr_extra_state_attributes: dict[str, Any] = {} + self._attr_extra_state_attributes = {} self._attr_should_poll = False self._attr_name = SIA_NAME_FORMAT.format( self._port, self._account, self._zone, self._attr_device_class diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8c134226f69..6383de15b4a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -246,7 +246,7 @@ class Entity(ABC): _attr_device_info: DeviceInfo | None = None _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool - _attr_extra_state_attributes: MutableMapping[str, Any] | None = None + _attr_extra_state_attributes: MutableMapping[str, Any] _attr_force_update: bool _attr_icon: str | None _attr_name: str | None @@ -319,7 +319,9 @@ class Entity(ABC): Implemented by platform classes. Convention for attribute names is lowercase snake_case. """ - return self._attr_extra_state_attributes + if hasattr(self, "_attr_extra_state_attributes"): + return self._attr_extra_state_attributes + return None @property def device_info(self) -> DeviceInfo | None: From 6376b4be5c51fd8791568372986c1a85b8526ceb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 26 Jul 2021 16:43:52 -0700 Subject: [PATCH 611/818] Increase static type coverage for nest integration (#53475) Co-authored-by: Mick Vleeshouwer Co-authored-by: Franck Nijhof --- homeassistant/components/nest/__init__.py | 6 +- homeassistant/components/nest/api.py | 4 +- .../components/nest/binary_sensor.py | 3 +- homeassistant/components/nest/camera.py | 3 +- homeassistant/components/nest/camera_sdm.py | 56 ++++++++++------- homeassistant/components/nest/climate.py | 3 +- homeassistant/components/nest/climate_sdm.py | 62 ++++++++++--------- homeassistant/components/nest/config_flow.py | 10 ++- homeassistant/components/nest/device_info.py | 34 +++++----- .../components/nest/device_trigger.py | 12 ++-- .../components/nest/legacy/__init__.py | 4 +- .../components/nest/legacy/binary_sensor.py | 2 +- .../components/nest/legacy/camera.py | 2 +- .../components/nest/legacy/climate.py | 2 +- .../components/nest/legacy/sensor.py | 2 +- homeassistant/components/nest/sensor.py | 3 +- homeassistant/components/nest/sensor_sdm.py | 32 +++++----- tests/components/nest/device_info_test.py | 14 ++--- tests/components/nest/sensor_sdm_test.py | 2 +- 19 files changed, 146 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 1548814804b..b999b2e94e0 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -69,7 +69,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["sensor", "camera", "climate"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up Nest components with dispatch between old/new flows.""" hass.data[DOMAIN] = {} @@ -109,7 +109,7 @@ class SignalUpdateCallback: """Initialize EventCallback.""" self._hass = hass - async def async_handle_event(self, event_message: EventMessage): + async def async_handle_event(self, event_message: EventMessage) -> None: """Process an incoming EventMessage.""" if not event_message.resource_update_name: return @@ -194,7 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if DATA_SDM not in entry.data: # Legacy API diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 29f39f5aec3..8affad958b7 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -29,13 +29,13 @@ class AsyncConfigEntryAuth(AbstractAuth): self._client_id = client_id self._client_secret = client_secret - async def async_get_access_token(self): + async def async_get_access_token(self) -> str: """Return a valid access token for SDM API.""" if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() return self._oauth_session.token["access_token"] - async def async_get_creds(self): + async def async_get_creds(self) -> Credentials: """Return an OAuth credential for Pub/Sub Subscriber.""" # We don't have a way for Home Assistant to refresh creds on behalf # of the google pub/sub subscriber. Instead, build a full diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 0bf65f2163c..6d9331744ef 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -2,13 +2,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SDM from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the binary sensors.""" assert DATA_SDM not in entry.data diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index ca117f0cbf1..7ae3e0db943 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +10,7 @@ from .legacy.camera import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the cameras.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index f8f2db506e2..862b6dbdffb 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -3,11 +3,14 @@ from __future__ import annotations import datetime import logging +from typing import Any, Callable from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, CameraLiveStreamTrait, + CameraMotionTrait, + RtspStream, ) from google_nest_sdm.device import Device from google_nest_sdm.exceptions import GoogleNestException @@ -18,11 +21,13 @@ from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from .const import DATA_SUBSCRIBER, DOMAIN -from .device_info import DeviceInfo +from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -31,7 +36,7 @@ STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the cameras.""" @@ -60,13 +65,13 @@ class NestCamera(Camera): """Initialize the camera.""" super().__init__() self._device = device - self._device_info = DeviceInfo(device) - self._stream = None - self._stream_refresh_unsub = None + self._device_info = NestDeviceInfo(device) + self._stream: RtspStream | None = None + self._stream_refresh_unsub: Callable[[], None] | None = None # Cache of most recent event image - self._event_id = None - self._event_image_bytes = None - self._event_image_cleanup_unsub = None + self._event_id: str | None = None + self._event_image_bytes: bytes | None = None + self._event_image_cleanup_unsub: Callable[[], None] | None = None @property def should_poll(self) -> bool: @@ -74,40 +79,40 @@ class NestCamera(Camera): return False @property - def unique_id(self) -> str | None: + def unique_id(self) -> str: """Return a unique ID.""" # The API "name" field is a unique device identifier. return f"{self._device.name}-camera" @property - def name(self): + def name(self) -> str | None: """Return the name of the camera.""" return self._device_info.device_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return self._device_info.device_info @property - def brand(self): + def brand(self) -> str | None: """Return the camera brand.""" return self._device_info.device_brand @property - def model(self): + def model(self) -> str | None: """Return the camera model.""" return self._device_info.device_model @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if CameraLiveStreamTrait.NAME in self._device.traits: supported_features |= SUPPORT_STREAM return supported_features - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the source of the stream.""" if CameraLiveStreamTrait.NAME not in self._device.traits: return None @@ -120,8 +125,9 @@ class NestCamera(Camera): _LOGGER.warning("Stream already expired") return self._stream.rtsp_stream_url - def _schedule_stream_refresh(self): + def _schedule_stream_refresh(self) -> None: """Schedules an alarm to refresh the stream url before expiration.""" + assert self._stream _LOGGER.debug("New stream url expires at %s", self._stream.expires_at) refresh_time = self._stream.expires_at - STREAM_EXPIRATION_BUFFER # Schedule an alarm to extend the stream @@ -134,7 +140,7 @@ class NestCamera(Camera): refresh_time, ) - async def _handle_stream_refresh(self, now): + async def _handle_stream_refresh(self, now: datetime.datetime) -> None: """Alarm that fires to check if the stream should be refreshed.""" if not self._stream: return @@ -154,7 +160,7 @@ class NestCamera(Camera): self.stream.update_source(self._stream.rtsp_stream_url) self._schedule_stream_refresh() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Invalidates the RTSP token when unloaded.""" if self._stream: _LOGGER.debug("Invalidating stream") @@ -166,13 +172,13 @@ class NestCamera(Camera): if self._event_image_cleanup_unsub is not None: self._event_image_cleanup_unsub() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) ) - async def async_camera_image(self): + async def async_camera_image(self) -> bytes | None: """Return bytes of camera image.""" # Returns the snapshot of the last event for ~30 seconds after the event active_event_image = await self._async_active_event_image() @@ -184,7 +190,7 @@ class NestCamera(Camera): return None return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) - async def _async_active_event_image(self): + async def _async_active_event_image(self) -> bytes | None: """Return image from any active events happening.""" if CameraEventImageTrait.NAME not in self._device.traits: return None @@ -204,7 +210,9 @@ class NestCamera(Camera): self._schedule_event_image_cleanup(event.expires_at) return image_bytes - async def _async_fetch_active_event_image(self, trait): + async def _async_fetch_active_event_image( + self, trait: CameraMotionTrait + ) -> bytes | None: """Return image bytes for an active event.""" try: event_image = await trait.generate_active_event_image() @@ -219,7 +227,7 @@ class NestCamera(Camera): _LOGGER.debug("Unable to fetch event image: %s", err) return None - def _schedule_event_image_cleanup(self, point_in_time): + def _schedule_event_image_cleanup(self, point_in_time: datetime.datetime) -> None: """Schedules an alarm to remove the image bytes from memory, honoring expiration.""" if self._event_image_cleanup_unsub is not None: self._event_image_cleanup_unsub() @@ -229,7 +237,7 @@ class NestCamera(Camera): point_in_time, ) - def _handle_event_image_cleanup(self, now): + def _handle_event_image_cleanup(self, now: Any) -> None: """Clear images cached from events and scheduled callback.""" self._event_id = None self._event_image_bytes = None diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 1644cc46004..372909d00c2 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +10,7 @@ from .legacy.climate import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the climate platform.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index ab987ff332f..51daa7fbb9c 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -1,6 +1,8 @@ """Support for Google Nest SDM climate devices.""" from __future__ import annotations +from typing import Any + from google_nest_sdm.device import Device from google_nest_sdm.device_traits import FanTrait, TemperatureTrait from google_nest_sdm.exceptions import GoogleNestException @@ -37,12 +39,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SUBSCRIBER, DOMAIN -from .device_info import DeviceInfo +from .device_info import NestDeviceInfo # Mapping for sdm.devices.traits.ThermostatMode mode field -THERMOSTAT_MODE_MAP = { +THERMOSTAT_MODE_MAP: dict[str, str] = { "OFF": HVAC_MODE_OFF, "HEAT": HVAC_MODE_HEAT, "COOL": HVAC_MODE_COOL, @@ -78,7 +82,7 @@ MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the client entities.""" @@ -101,7 +105,7 @@ class ThermostatEntity(ClimateEntity): def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" self._device = device - self._device_info = DeviceInfo(device) + self._device_info = NestDeviceInfo(device) self._supported_features = 0 @property @@ -116,16 +120,16 @@ class ThermostatEntity(ClimateEntity): return self._device.name @property - def name(self): + def name(self) -> str | None: """Return the name of the entity.""" return self._device_info.device_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return self._device_info.device_info - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self._supported_features = self._get_supported_features() self.async_on_remove( @@ -133,20 +137,20 @@ class ThermostatEntity(ClimateEntity): ) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of temperature measurement for the system.""" return TEMP_CELSIUS @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" if TemperatureTrait.NAME not in self._device.traits: return None - trait = self._device.traits[TemperatureTrait.NAME] + trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature currently set to be reached.""" trait = self._target_temperature_trait if not trait: @@ -158,7 +162,7 @@ class ThermostatEntity(ClimateEntity): return None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the upper bound target temperature.""" if self.hvac_mode != HVAC_MODE_HEAT_COOL: return None @@ -168,7 +172,7 @@ class ThermostatEntity(ClimateEntity): return trait.cool_celsius @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lower bound target temperature.""" if self.hvac_mode != HVAC_MODE_HEAT_COOL: return None @@ -178,7 +182,9 @@ class ThermostatEntity(ClimateEntity): return trait.heat_celsius @property - def _target_temperature_trait(self): + def _target_temperature_trait( + self, + ) -> ThermostatEcoTrait | ThermostatTemperatureSetpointTrait | None: """Return the correct trait with a target temp depending on mode.""" if ( self.preset_mode == PRESET_ECO @@ -190,7 +196,7 @@ class ThermostatEntity(ClimateEntity): return None @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return the current operation (e.g. heat, cool, idle).""" hvac_mode = HVAC_MODE_OFF if ThermostatModeTrait.NAME in self._device.traits: @@ -202,7 +208,7 @@ class ThermostatEntity(ClimateEntity): return hvac_mode @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """List of available operation modes.""" supported_modes = [] for mode in self._get_device_hvac_modes: @@ -213,7 +219,7 @@ class ThermostatEntity(ClimateEntity): return supported_modes @property - def _get_device_hvac_modes(self): + def _get_device_hvac_modes(self) -> set[str]: """Return the set of SDM API hvac modes supported by the device.""" modes = [] if ThermostatModeTrait.NAME in self._device.traits: @@ -222,7 +228,7 @@ class ThermostatEntity(ClimateEntity): return set(modes) @property - def hvac_action(self): + def hvac_action(self) -> str | None: """Return the current HVAC action (heating, cooling).""" trait = self._device.traits[ThermostatHvacTrait.NAME] if trait.status in THERMOSTAT_HVAC_STATUS_MAP: @@ -230,7 +236,7 @@ class ThermostatEntity(ClimateEntity): return None @property - def preset_mode(self): + def preset_mode(self) -> str: """Return the current active preset.""" if ThermostatEcoTrait.NAME in self._device.traits: trait = self._device.traits[ThermostatEcoTrait.NAME] @@ -238,7 +244,7 @@ class ThermostatEntity(ClimateEntity): return PRESET_NONE @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return the available presets.""" modes = [] if ThermostatEcoTrait.NAME in self._device.traits: @@ -249,7 +255,7 @@ class ThermostatEntity(ClimateEntity): return modes @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the current fan mode.""" if FanTrait.NAME in self._device.traits: trait = self._device.traits[FanTrait.NAME] @@ -257,7 +263,7 @@ class ThermostatEntity(ClimateEntity): return FAN_OFF @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" modes = [] if FanTrait.NAME in self._device.traits: @@ -265,11 +271,11 @@ class ThermostatEntity(ClimateEntity): return modes @property - def supported_features(self): + def supported_features(self) -> int: """Bitmap of supported features.""" return self._supported_features - def _get_supported_features(self): + def _get_supported_features(self) -> int: """Compute the bitmap of supported features from the current state.""" features = 0 if HVAC_MODE_HEAT_COOL in self.hvac_modes: @@ -285,7 +291,7 @@ class ThermostatEntity(ClimateEntity): features |= SUPPORT_FAN_MODE return features - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") @@ -297,7 +303,7 @@ class ThermostatEntity(ClimateEntity): trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -313,14 +319,14 @@ class ThermostatEntity(ClimateEntity): elif self.hvac_mode == HVAC_MODE_HEAT and temp: await trait.set_heat(temp) - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index eeeae4b1ddd..1ec3e421a0d 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -35,7 +35,13 @@ _LOGGER = logging.getLogger(__name__) @callback -def register_flow_implementation(hass, domain, name, gen_authorize_url, convert_code): +def register_flow_implementation( + hass: HomeAssistant, + domain: str, + name: str, + gen_authorize_url: str, + convert_code: str, +) -> None: """Register a flow implementation for legacy api. domain: Domain of the component responsible for the implementation. @@ -74,7 +80,7 @@ class NestFlowHandler( DOMAIN = DOMAIN VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize NestFlowHandler.""" super().__init__() # When invoked for reauth, allows updating an existing config entry diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 579733de8ad..7b10fabcd61 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -1,11 +1,15 @@ """Library for extracting device specific information common to entities.""" +from __future__ import annotations + from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait +from homeassistant.helpers.entity import DeviceInfo + from .const import DOMAIN -DEVICE_TYPE_MAP = { +DEVICE_TYPE_MAP: dict[str, str] = { "sdm.devices.types.CAMERA": "Camera", "sdm.devices.types.DISPLAY": "Display", "sdm.devices.types.DOORBELL": "Doorbell", @@ -13,7 +17,7 @@ DEVICE_TYPE_MAP = { } -class DeviceInfo: +class NestDeviceInfo: """Provide device info from the SDM device, shared across platforms.""" device_brand = "Google Nest" @@ -23,21 +27,23 @@ class DeviceInfo: self._device = device @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - # The API "name" field is a unique device identifier. - "identifiers": {(DOMAIN, self._device.name)}, - "name": self.device_name, - "manufacturer": self.device_brand, - "model": self.device_model, - } + return DeviceInfo( + { + # The API "name" field is a unique device identifier. + "identifiers": {(DOMAIN, self._device.name)}, + "name": self.device_name, + "manufacturer": self.device_brand, + "model": self.device_model, + } + ) @property - def device_name(self): + def device_name(self) -> str: """Return the name of the physical device that includes the sensor.""" if InfoTrait.NAME in self._device.traits: - trait = self._device.traits[InfoTrait.NAME] + trait: InfoTrait = self._device.traits[InfoTrait.NAME] if trait.custom_name: return trait.custom_name # Build a name from the room/structure. Note: This room/structure name @@ -50,9 +56,9 @@ class DeviceInfo: return self.device_model @property - def device_model(self): + def device_model(self) -> str: """Return device model information.""" # The API intentionally returns minimal information about specific # devices, instead relying on traits, but we can infer a generic model # name based on the type - return DEVICE_TYPE_MAP.get(self._device.type) + return DEVICE_TYPE_MAP.get(self._device.type, "Unknown") diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 889111d6f61..980d9726467 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -11,6 +11,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.typing import ConfigType from .const import DATA_SUBSCRIBER, DOMAIN @@ -29,11 +30,14 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: """Get the nest API device_id from the HomeAssistant device_id.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry: DeviceRegistry = ( + await hass.helpers.device_registry.async_get_registry() + ) device = device_registry.async_get(device_id) - for (domain, unique_id) in device.identifiers: - if domain == DOMAIN: - return unique_id + if device: + for (domain, unique_id) in device.identifiers: + if domain == DOMAIN: + return unique_id return None diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index b0083dcf990..04f7b1ac663 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -96,7 +96,7 @@ def nest_update_event_broker(hass, nest): _LOGGER.debug("Stop listening for nest.update_event") -async def async_setup_legacy(hass, config): +async def async_setup_legacy(hass, config) -> bool: """Set up Nest components using the legacy nest API.""" if DOMAIN not in config: return True @@ -122,7 +122,7 @@ async def async_setup_legacy(hass, config): return True -async def async_setup_legacy_entry(hass, entry): +async def async_setup_legacy_entry(hass, entry) -> bool: """Set up Nest from legacy config entry.""" nest = Nest(access_token=entry.data["tokens"]["access_token"]) diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py index 32c30f747d2..c257ddd9456 100644 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up a Nest binary sensor based on a config entry.""" nest = hass.data[DATA_NEST] diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index cc9be9d7588..77629e4dcff 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up a Nest sensor based on a config entry.""" camera_devices = await hass.async_add_executor_job(hass.data[DATA_NEST].cameras) cameras = [NestCamera(structure, device) for structure, device in camera_devices] diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index cd0d66acba8..17448d9be8c 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -74,7 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up the Nest climate device based on a config entry.""" temp_unit = hass.config.units.temperature_unit diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 54df3921bbd..0939e925b43 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up a Nest sensor based on a config entry.""" nest = hass.data[DATA_NEST] diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index c58ad26112d..a9073aec80d 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SDM from .legacy.sensor import async_setup_legacy_entry @@ -9,7 +10,7 @@ from .sensor_sdm import async_setup_sdm_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index d1ff26880de..42614af8c40 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -17,9 +17,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SUBSCRIBER, DOMAIN -from .device_info import DeviceInfo +from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,7 @@ DEVICE_TYPE_MAP = { async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" @@ -59,7 +61,7 @@ class SensorBase(SensorEntity): def __init__(self, device: Device) -> None: """Initialize the sensor.""" self._device = device - self._device_info = DeviceInfo(device) + self._device_info = NestDeviceInfo(device) @property def should_poll(self) -> bool: @@ -73,11 +75,11 @@ class SensorBase(SensorEntity): return f"{self._device.name}-{self.device_class}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return self._device_info.device_info - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) @@ -88,23 +90,23 @@ class TemperatureSensor(SensorBase): """Representation of a Temperature Sensor.""" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._device_info.device_name} Temperature" @property - def state(self): + def state(self) -> float: """Return the state of the sensor.""" - trait = self._device.traits[TemperatureTrait.NAME] + trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device.""" return DEVICE_CLASS_TEMPERATURE @@ -119,22 +121,22 @@ class HumiditySensor(SensorBase): return f"{self._device.name}-humidity" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._device_info.device_name} Humidity" @property - def state(self): + def state(self) -> float: """Return the state of the sensor.""" - trait = self._device.traits[HumidityTrait.NAME] + trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] return trait.ambient_humidity_percent @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return PERCENTAGE @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device.""" return DEVICE_CLASS_HUMIDITY diff --git a/tests/components/nest/device_info_test.py b/tests/components/nest/device_info_test.py index 1561364d348..a0c6973c1d6 100644 --- a/tests/components/nest/device_info_test.py +++ b/tests/components/nest/device_info_test.py @@ -2,7 +2,7 @@ from google_nest_sdm.device import Device -from homeassistant.components.nest.device_info import DeviceInfo +from homeassistant.components.nest.device_info import NestDeviceInfo def test_device_custom_name(): @@ -20,7 +20,7 @@ def test_device_custom_name(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "My Doorbell" assert device_info.device_model == "Doorbell" assert device_info.device_brand == "Google Nest" @@ -45,7 +45,7 @@ def test_device_name_room(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "Some Room" assert device_info.device_model == "Doorbell" assert device_info.device_brand == "Google Nest" @@ -64,7 +64,7 @@ def test_device_no_name(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "Doorbell" assert device_info.device_model == "Doorbell" assert device_info.device_brand == "Google Nest" @@ -91,13 +91,13 @@ def test_device_invalid_type(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "My Doorbell" - assert device_info.device_model is None + assert device_info.device_model == "Unknown" assert device_info.device_brand == "Google Nest" assert device_info.device_info == { "identifiers": {("nest", "some-device-id")}, "name": "My Doorbell", "manufacturer": "Google Nest", - "model": None, + "model": "Unknown", } diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index dfdfd58d546..cc18e8cd3ae 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -208,5 +208,5 @@ async def test_device_with_unknown_type(hass): device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) assert device.name == "My Sensor" - assert device.model is None + assert device.model == "Unknown" assert device.identifiers == {("nest", "some-device-id")} From 3e08e500508c0579ff043a77f4a4538cf832d438 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 01:46:01 +0200 Subject: [PATCH 612/818] Use EntityDescription - nws (#53523) * Use EntityDescription - nws * Bugfix * Bugfix 2 --- homeassistant/components/nws/const.py | 95 ++++++++++++++------------ homeassistant/components/nws/sensor.py | 58 +++++++--------- tests/components/nws/test_sensor.py | 24 ++++--- 3 files changed, 91 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index b5814613847..6e08ef408d3 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,9 +1,10 @@ """Constants for National Weather Service Integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta -from typing import NamedTuple +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, @@ -99,92 +100,100 @@ OBSERVATION_VALID_TIME = timedelta(minutes=20) FORECAST_VALID_TIME = timedelta(minutes=45) -class NWSSensorMetadata(NamedTuple): - """Sensor metadata for an individual NWS sensor.""" +@dataclass +class NWSSensorEntityDescription(SensorEntityDescription): + """Class describing NWSSensor entities.""" - label: str - icon: str | None - device_class: str | None - unit: str - unit_convert: str + unit_convert: str | None = None -SENSOR_TYPES: dict[str, NWSSensorMetadata] = { - "dewpoint": NWSSensorMetadata( - "Dew Point", +SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( + NWSSensorEntityDescription( + key="dewpoint", + name="Dew Point", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), - "temperature": NWSSensorMetadata( - "Temperature", + NWSSensorEntityDescription( + key="temperature", + name="Temperature", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), - "windChill": NWSSensorMetadata( - "Wind Chill", + NWSSensorEntityDescription( + key="windChill", + name="Wind Chill", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), - "heatIndex": NWSSensorMetadata( - "Heat Index", + NWSSensorEntityDescription( + key="heatIndex", + name="Heat Index", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), - "relativeHumidity": NWSSensorMetadata( - "Relative Humidity", + NWSSensorEntityDescription( + key="relativeHumidity", + name="Relative Humidity", icon=None, device_class=DEVICE_CLASS_HUMIDITY, - unit=PERCENTAGE, + unit_of_measurement=PERCENTAGE, unit_convert=PERCENTAGE, ), - "windSpeed": NWSSensorMetadata( - "Wind Speed", + NWSSensorEntityDescription( + key="windSpeed", + name="Wind Speed", icon="mdi:weather-windy", device_class=None, - unit=SPEED_KILOMETERS_PER_HOUR, + unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), - "windGust": NWSSensorMetadata( - "Wind Gust", + NWSSensorEntityDescription( + key="windGust", + name="Wind Gust", icon="mdi:weather-windy", device_class=None, - unit=SPEED_KILOMETERS_PER_HOUR, + unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), - "windDirection": NWSSensorMetadata( - "Wind Direction", + NWSSensorEntityDescription( + key="windDirection", + name="Wind Direction", icon="mdi:compass-rose", device_class=None, - unit=DEGREE, + unit_of_measurement=DEGREE, unit_convert=DEGREE, ), - "barometricPressure": NWSSensorMetadata( - "Barometric Pressure", + NWSSensorEntityDescription( + key="barometricPressure", + name="Barometric Pressure", icon=None, device_class=DEVICE_CLASS_PRESSURE, - unit=PRESSURE_PA, + unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), - "seaLevelPressure": NWSSensorMetadata( - "Sea Level Pressure", + NWSSensorEntityDescription( + key="seaLevelPressure", + name="Sea Level Pressure", icon=None, device_class=DEVICE_CLASS_PRESSURE, - unit=PRESSURE_PA, + unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), - "visibility": NWSSensorMetadata( - "Visibility", + NWSSensorEntityDescription( + key="visibility", + name="Visibility", icon="mdi:eye", device_class=None, - unit=LENGTH_METERS, + unit_of_measurement=LENGTH_METERS, unit_convert=LENGTH_MILES, ), -} +) diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 8bbf6af8057..409856831a2 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -28,7 +28,7 @@ from .const import ( NWS_DATA, OBSERVATION_VALID_TIME, SENSOR_TYPES, - NWSSensorMetadata, + NWSSensorEntityDescription, ) PARALLEL_UPDATES = 0 @@ -39,32 +39,29 @@ async def async_setup_entry(hass, entry, async_add_entities): hass_data = hass.data[DOMAIN][entry.entry_id] station = entry.data[CONF_STATION] - entities = [] - for sensor_type, metadata in SENSOR_TYPES.items(): - entities.append( - NWSSensor( - hass, - entry.data, - hass_data, - sensor_type, - metadata, - station, - ), + async_add_entities( + NWSSensor( + hass=hass, + entry_data=entry.data, + hass_data=hass_data, + description=description, + station=station, ) - - async_add_entities(entities, False) + for description in SENSOR_TYPES + ) class NWSSensor(CoordinatorEntity, SensorEntity): """An NWS Sensor Entity.""" + entity_description: NWSSensorEntityDescription + def __init__( self, hass: HomeAssistant, entry_data, hass_data, - sensor_type, - metadata: NWSSensorMetadata, + description: NWSSensorEntityDescription, station, ): """Initialise the platform with a data instance.""" @@ -72,32 +69,29 @@ class NWSSensor(CoordinatorEntity, SensorEntity): self._nws = hass_data[NWS_DATA] self._latitude = entry_data[CONF_LATITUDE] self._longitude = entry_data[CONF_LONGITUDE] - self._type = sensor_type - self._metadata = metadata + self.entity_description = description - self._attr_name = f"{station} {metadata.label}" - self._attr_icon = metadata.icon - self._attr_device_class = metadata.device_class - if hass.config.units.is_metric: - self._attr_unit_of_measurement = metadata.unit - else: - self._attr_unit_of_measurement = metadata.unit_convert + self._attr_name = f"{station} {description.name}" + if not hass.config.units.is_metric: + self._attr_unit_of_measurement = description.unit_convert @property def state(self): """Return the state.""" - value = self._nws.observation.get(self._type) + value = self._nws.observation.get(self.entity_description.key) if value is None: return None - if self._attr_unit_of_measurement == SPEED_MILES_PER_HOUR: + # Set alias to unit property -> prevent unnecessary hasattr calls + unit_of_measurement = self.unit_of_measurement + if unit_of_measurement == SPEED_MILES_PER_HOUR: return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) - if self._attr_unit_of_measurement == LENGTH_MILES: + if unit_of_measurement == LENGTH_MILES: return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) - if self._attr_unit_of_measurement == PRESSURE_INHG: + if unit_of_measurement == PRESSURE_INHG: return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) - if self._attr_unit_of_measurement == TEMP_CELSIUS: + if unit_of_measurement == TEMP_CELSIUS: return round(value, 1) - if self._attr_unit_of_measurement == PERCENTAGE: + if unit_of_measurement == PERCENTAGE: return round(value) return value @@ -109,7 +103,7 @@ class NWSSensor(CoordinatorEntity, SensorEntity): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{base_unique_id(self._latitude, self._longitude)}_{self._type}" + return f"{base_unique_id(self._latitude, self._longitude)}_{self.entity_description.key}" @property def available(self): diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py index f5c0773380d..aa5ca3bf66c 100644 --- a/tests/components/nws/test_sensor.py +++ b/tests/components/nws/test_sensor.py @@ -35,12 +35,12 @@ async def test_imperial_metric( """Test with imperial and metric units.""" registry = await hass.helpers.entity_registry.async_get_registry() - for sensor_name, metadata in SENSOR_TYPES.items(): + for description in SENSOR_TYPES: registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, - f"35_-75_{sensor_name}", - suggested_object_id=f"abc_{metadata.label}", + f"35_-75_{description.key}", + suggested_object_id=f"abc_{description.name}", disabled_by=None, ) @@ -53,10 +53,11 @@ async def test_imperial_metric( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for sensor_name, metadata in SENSOR_TYPES.items(): - state = hass.states.get(f"sensor.abc_{slugify(metadata.label)}") + for description in SENSOR_TYPES: + assert description.name + state = hass.states.get(f"sensor.abc_{slugify(description.name)}") assert state - assert state.state == result_observation[sensor_name] + assert state.state == result_observation[description.key] assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -67,12 +68,12 @@ async def test_none_values(hass, mock_simple_nws, no_weather): registry = await hass.helpers.entity_registry.async_get_registry() - for sensor_name, metadata in SENSOR_TYPES.items(): + for description in SENSOR_TYPES: registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, - f"35_-75_{sensor_name}", - suggested_object_id=f"abc_{metadata.label}", + f"35_-75_{description.key}", + suggested_object_id=f"abc_{description.name}", disabled_by=None, ) @@ -84,7 +85,8 @@ async def test_none_values(hass, mock_simple_nws, no_weather): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for sensor_name, metadata in SENSOR_TYPES.items(): - state = hass.states.get(f"sensor.abc_{slugify(metadata.label)}") + for description in SENSOR_TYPES: + assert description.name + state = hass.states.get(f"sensor.abc_{slugify(description.name)}") assert state assert state.state == STATE_UNKNOWN From 20b66faa6d2256d5a6a5f29cd26eb01f90c91de2 Mon Sep 17 00:00:00 2001 From: BreakingBread0 <49913490+BreakingBread0@users.noreply.github.com> Date: Tue, 27 Jul 2021 01:46:49 +0200 Subject: [PATCH 613/818] Add MFA Capability to Tesla Integration (#53245) * Adds MFA Capability to Tesla Integration * Add "(Optional)" to MFA Code * Update homeassistant/components/tesla/translations/de.json Co-authored-by: J. Nick Koston * Update en.json * Revert "Update en.json" This reverts commit 825685c3a230f54094c10c86d7b82f4c81979064. Co-authored-by: J. Nick Koston --- homeassistant/components/tesla/config_flow.py | 6 +++++- homeassistant/components/tesla/const.py | 1 + homeassistant/components/tesla/strings.json | 3 ++- homeassistant/components/tesla/translations/de.json | 2 +- homeassistant/components/tesla/translations/en.json | 3 ++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index af2fd7ae769..46bc49b126b 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT from .const import ( CONF_EXPIRATION, + CONF_MFA, CONF_WAKE_ON_START, DEFAULT_SCAN_INTERVAL, DEFAULT_WAKE_ON_START, @@ -99,6 +100,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required(CONF_USERNAME, default=self.username): str, vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_MFA): str, } ) @@ -158,7 +160,9 @@ async def validate_input(hass: core.HomeAssistant, data): password=data[CONF_PASSWORD], update_interval=DEFAULT_SCAN_INTERVAL, ) - result = await controller.connect(test_login=True) + result = await controller.connect( + test_login=True, mfa_code=(data[CONF_MFA] if CONF_MFA in data else "") + ) config[CONF_TOKEN] = result["refresh_token"] config[CONF_ACCESS_TOKEN] = result["access_token"] config[CONF_EXPIRATION] = result[CONF_EXPIRATION] diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 4155942c0ad..c288b3c1cda 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -1,6 +1,7 @@ """Const file for Tesla cars.""" CONF_EXPIRATION = "expiration" CONF_WAKE_ON_START = "enable_wake_on_start" +CONF_MFA = "mfa" DOMAIN = "tesla" DATA_LISTENER = "listener" DEFAULT_SCAN_INTERVAL = 660 diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json index c75562528de..0f5a7666175 100644 --- a/homeassistant/components/tesla/strings.json +++ b/homeassistant/components/tesla/strings.json @@ -13,7 +13,8 @@ "user": { "data": { "username": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "mfa": "MFA Code (Optional)" }, "description": "Please enter your information.", "title": "Tesla - Configuration" diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index bdcd8237b3b..bb2a2a5f349 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -30,4 +30,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tesla/translations/en.json b/homeassistant/components/tesla/translations/en.json index 53b213ac19b..c8226a12347 100644 --- a/homeassistant/components/tesla/translations/en.json +++ b/homeassistant/components/tesla/translations/en.json @@ -13,7 +13,8 @@ "user": { "data": { "password": "Password", - "username": "Email" + "username": "Email", + "mfa": "MFA Code (Optional)" }, "description": "Please enter your information.", "title": "Tesla - Configuration" From 2b3148296c7af2dd381b48bd6c5aa2af5fdfac1b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 27 Jul 2021 00:12:20 +0000 Subject: [PATCH 614/818] [ci skip] Translation update --- .../components/energy/translations/ca.json | 3 ++ .../components/energy/translations/et.json | 3 ++ .../components/litejet/translations/de.json | 10 +++++++ .../components/tesla/translations/de.json | 2 +- .../components/tesla/translations/en.json | 4 +-- .../yale_smart_alarm/translations/de.json | 28 +++++++++++++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/energy/translations/ca.json create mode 100644 homeassistant/components/energy/translations/et.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/de.json diff --git a/homeassistant/components/energy/translations/ca.json b/homeassistant/components/energy/translations/ca.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/et.json b/homeassistant/components/energy/translations/et.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/de.json b/homeassistant/components/litejet/translations/de.json index ff528dd79b2..e3788821e58 100644 --- a/homeassistant/components/litejet/translations/de.json +++ b/homeassistant/components/litejet/translations/de.json @@ -15,5 +15,15 @@ "title": "Verbinde zu LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Standard\u00fcbergang (Sekunden)" + }, + "title": "LiteJet konfigurieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index bb2a2a5f349..bdcd8237b3b 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -30,4 +30,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/en.json b/homeassistant/components/tesla/translations/en.json index c8226a12347..16a1c185138 100644 --- a/homeassistant/components/tesla/translations/en.json +++ b/homeassistant/components/tesla/translations/en.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { + "mfa": "MFA Code (Optional)", "password": "Password", - "username": "Email", - "mfa": "MFA Code (Optional)" + "username": "Email" }, "description": "Please enter your information.", "title": "Tesla - Configuration" diff --git a/homeassistant/components/yale_smart_alarm/translations/de.json b/homeassistant/components/yale_smart_alarm/translations/de.json new file mode 100644 index 00000000000..b3434a70b7e --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Bereichs-ID", + "name": "Name", + "password": "Passwort", + "username": "Benutzername" + } + }, + "user": { + "data": { + "area_id": "Bereichs-ID", + "name": "Name", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file From d4d791e0a19ef29b860f603a3b8638462d933685 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 27 Jul 2021 05:23:25 +0200 Subject: [PATCH 615/818] Ensure Jewish Calendar returns an iso formatted timestamp (#52722) --- homeassistant/components/jewish_calendar/sensor.py | 7 ++++++- tests/components/jewish_calendar/test_sensor.py | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 0dfc61970ef..17a61c932a3 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,4 +1,5 @@ """Platform to retrieve Jewish calendar information for Home Assistant.""" +from datetime import datetime import logging import hdate @@ -51,6 +52,8 @@ class JewishCalendarSensor(SensorEntity): @property def state(self): """Return the state of the sensor.""" + if isinstance(self._state, datetime): + return self._state.isoformat() return self._state async def async_update(self): @@ -133,7 +136,9 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): @property def state(self): """Return the state of the sensor.""" - return dt_util.as_utc(self._state) if self._state is not None else None + if self._state is None: + return None + return dt_util.as_utc(self._state).isoformat() @property def extra_state_attributes(self): diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 970e31c7985..e9471d5d144 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -188,13 +188,13 @@ async def test_jewish_calendar_sensor( await hass.async_block_till_done() result = ( - dt_util.as_utc(result.replace(tzinfo=time_zone)) + dt_util.as_utc(result.replace(tzinfo=time_zone)).isoformat() if isinstance(result, dt) else result ) sensor_object = hass.states.get(f"sensor.test_{sensor}") - assert sensor_object.state == str(result) + assert sensor_object.state == result if sensor == "holiday": assert sensor_object.attributes.get("id") == "rosh_hashana_i" @@ -544,7 +544,7 @@ async def test_shabbat_times_sensor( sensor_type = sensor_type.replace(f"{language}_", "") result_value = ( - dt_util.as_utc(result_value) + dt_util.as_utc(result_value).isoformat() if isinstance(result_value, dt) else result_value ) From 711928520115ba82f0456dd95236a9a530c0aa67 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 26 Jul 2021 22:18:05 -0700 Subject: [PATCH 616/818] Clean wemo sensor attributes (#53532) * Wemo: Use the available property instead of returning unavailable from state property * Nit: s/_insight_device_key/_insight_params_key * Use insight_params_key for generating unique_id --- homeassistant/components/wemo/sensor.py | 66 +++++++++++-------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 426cb80adce..96856f0a140 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -9,7 +9,6 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, - STATE_UNAVAILABLE, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import StateType @@ -52,21 +51,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class InsightSensor(WemoSubscriptionEntity, SensorEntity): """Common base for WeMo Insight power sensors.""" - def __init__( - self, - device: DeviceWrapper, - update_insight_params: Callable, - name_suffix: str, - device_class: str, - unit_of_measurement: str, - ) -> None: + _attr_state_class = STATE_CLASS_MEASUREMENT + _name_suffix: str + _insight_params_key: str + + def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None: """Initialize the WeMo Insight power sensor.""" super().__init__(device) self._update_insight_params = update_insight_params - self._name_suffix = name_suffix - self._attr_device_class = device_class - self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_unit_of_measurement = unit_of_measurement @property def name(self) -> str: @@ -76,7 +68,14 @@ class InsightSensor(WemoSubscriptionEntity, SensorEntity): @property def unique_id(self) -> str: """Return the id of this entity.""" - return f"{self.wemo.serialnumber}_{self._name_suffix}" + return f"{self.wemo.serialnumber}_{self._insight_params_key}" + + @property + def available(self) -> str: + """Return true if sensor is available.""" + return ( + self._insight_params_key in self.wemo.insight_params and super().available + ) def _update(self, force_update=True) -> None: with self._wemo_exception_handler("update status"): @@ -87,36 +86,27 @@ class InsightSensor(WemoSubscriptionEntity, SensorEntity): class InsightCurrentPower(InsightSensor): """Current instantaineous power consumption.""" - def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None: - """Initialize the WeMo Insight power sensor.""" - super().__init__( - device, - update_insight_params, - "Current Power", - DEVICE_CLASS_POWER, - POWER_WATT, - ) + _attr_device_class = DEVICE_CLASS_POWER + _attr_unit_of_measurement = POWER_WATT + _name_suffix = "Current Power" + _insight_params_key = "currentpower" @property def state(self) -> StateType: """Return the current power consumption.""" - if "currentpower" not in self.wemo.insight_params: - return STATE_UNAVAILABLE - return convert(self.wemo.insight_params["currentpower"], float, 0.0) / 1000.0 + return ( + convert(self.wemo.insight_params[self._insight_params_key], float, 0.0) + / 1000.0 + ) class InsightTodayEnergy(InsightSensor): """Energy used today.""" - def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None: - """Initialize the WeMo Insight power sensor.""" - super().__init__( - device, - update_insight_params, - "Today Energy", - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, - ) + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _name_suffix = "Today Energy" + _insight_params_key = "todaymw" @property def last_reset(self) -> datetime: @@ -126,7 +116,7 @@ class InsightTodayEnergy(InsightSensor): @property def state(self) -> StateType: """Return the current energy use today.""" - if "todaymw" not in self.wemo.insight_params: - return STATE_UNAVAILABLE - miliwatts = convert(self.wemo.insight_params["todaymw"], float, 0.0) + miliwatts = convert( + self.wemo.insight_params[self._insight_params_key], float, 0.0 + ) return round(miliwatts / (1000.0 * 1000.0 * 60), 2) From d7d859fc86936a12d662b1b83b46fd5624a46677 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 27 Jul 2021 01:40:20 -0700 Subject: [PATCH 617/818] Update nexia to 0.9.11 (#53534) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index a453ec7f1df..105cbdb62b7 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.10"], + "requirements": ["nexia==0.9.11"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index a265465a05f..83e8ef37bee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1025,7 +1025,7 @@ nettigo-air-monitor==1.0.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.10 +nexia==0.9.11 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af47d7d196f..7815d596c20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -582,7 +582,7 @@ netdisco==2.9.0 nettigo-air-monitor==1.0.0 # homeassistant.components.nexia -nexia==0.9.10 +nexia==0.9.11 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.2 From 5483ab0cda14e18a33546a3dffe4cf77b0327435 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 27 Jul 2021 02:42:51 -0600 Subject: [PATCH 618/818] Enforce strict typing for Flu Near You (#53407) --- .strict-typing | 1 + .../components/flunearyou/__init__.py | 22 ++++++++---- .../components/flunearyou/config_flow.py | 13 +++++-- .../components/flunearyou/manifest.json | 2 +- homeassistant/components/flunearyou/sensor.py | 35 ++++++++++++++----- mypy.ini | 11 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 67 insertions(+), 21 deletions(-) diff --git a/.strict-typing b/.strict-typing index 90d2d135de3..82069f1548a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -37,6 +37,7 @@ homeassistant.components.esphome.* homeassistant.components.energy.* homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* +homeassistant.components.flunearyou.* homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* homeassistant.components.frontend.* diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index d80591c067c..22de54180a6 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -1,12 +1,17 @@ """The flunearyou component.""" +from __future__ import annotations + import asyncio from datetime import timedelta from functools import partial +from typing import Any from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -25,28 +30,33 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["sensor"] -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} websession = aiohttp_client.async_get_clientsession(hass) - client = Client(websession) + client = Client(session=websession) latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) - async def async_update(api_category): + async def async_update(api_category: str) -> dict[str, Any]: """Get updated date from the API based on category.""" try: if api_category == CATEGORY_CDC_REPORT: - return await client.cdc_reports.status_by_coordinates( + data = await client.cdc_reports.status_by_coordinates( + latitude, longitude + ) + else: + data = await client.user_reports.status_by_coordinates( latitude, longitude ) - return await client.user_reports.status_by_coordinates(latitude, longitude) except FluNearYouError as err: raise UpdateFailed(err) from err + return data + data_init_tasks = [] for api_category in (CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT): coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ @@ -67,7 +77,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Flu Near You config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py index a63b6484a61..0005e0c257a 100644 --- a/homeassistant/components/flunearyou/config_flow.py +++ b/homeassistant/components/flunearyou/config_flow.py @@ -1,10 +1,15 @@ """Define a config flow manager for flunearyou.""" +from __future__ import annotations + +from typing import Any + from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER @@ -16,7 +21,7 @@ class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 @property - def data_schema(self): + def data_schema(self) -> vol.Schema: """Return the data schema for integration.""" return vol.Schema( { @@ -29,7 +34,9 @@ class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form(step_id="user", data_schema=self.data_schema) @@ -40,7 +47,7 @@ class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(websession) + client = Client(session=websession) try: await client.cdc_reports.status_by_coordinates( diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index 71f0b49771e..5fd3eb6638f 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -3,7 +3,7 @@ "name": "Flu Near You", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", - "requirements": ["pyflunearyou==1.0.7"], + "requirements": ["pyflunearyou==2.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 244d9120d7d..88fb0147296 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -1,13 +1,20 @@ """Support for user- and CDC-based flu info sensors from Flu Near You.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_STATE, CONF_LATITUDE, CONF_LONGITUDE, ) -from homeassistant.core import callback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DATA_COORDINATOR, DOMAIN @@ -53,11 +60,13 @@ EXTENDED_SENSOR_TYPE_MAPPING = { } -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Flu Near You sensors based on a config entry.""" coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] - sensors = [] + sensors: list[CdcSensor | UserSensor] = [] for (sensor_type, name, icon, unit) in CDC_SENSORS: sensors.append( @@ -89,7 +98,15 @@ async def async_setup_entry(hass, entry, async_add_entities): class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" - def __init__(self, coordinator, entry, sensor_type, name, icon, unit): + def __init__( + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + sensor_type: str, + name: str, + icon: str, + unit: str | None, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} @@ -109,13 +126,13 @@ class FluNearYouSensor(CoordinatorEntity, SensorEntity): self.update_from_latest_data() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() self.update_from_latest_data() @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the sensor.""" raise NotImplementedError @@ -124,7 +141,7 @@ class CdcSensor(FluNearYouSensor): """Define a sensor for CDC reports.""" @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the sensor.""" self._attr_extra_state_attributes.update( { @@ -139,7 +156,7 @@ class UserSensor(FluNearYouSensor): """Define a sensor for user reports.""" @callback - def update_from_latest_data(self): + def update_from_latest_data(self) -> None: """Update the sensor.""" self._attr_extra_state_attributes.update( { diff --git a/mypy.ini b/mypy.ini index beab8cd8d17..072242f44e7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -418,6 +418,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.flunearyou.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.forecast_solar.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 83e8ef37bee..d9c8162d26d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1446,7 +1446,7 @@ pyflic==2.0.3 pyflume==0.5.5 # homeassistant.components.flunearyou -pyflunearyou==1.0.7 +pyflunearyou==2.0.2 # homeassistant.components.futurenow pyfnip==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7815d596c20..6704cb73b5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -811,7 +811,7 @@ pyfireservicerota==0.0.43 pyflume==0.5.5 # homeassistant.components.flunearyou -pyflunearyou==1.0.7 +pyflunearyou==2.0.2 # homeassistant.components.forked_daapd pyforked-daapd==0.1.11 From a6b34924be56ba5d2079e44ac46361fd199952f3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 27 Jul 2021 02:45:44 -0600 Subject: [PATCH 619/818] Enforce strict typing for RainMachine (#53414) --- .strict-typing | 1 + .../components/rainmachine/__init__.py | 34 +++++++------ .../components/rainmachine/config_flow.py | 51 +++++++++++-------- .../components/rainmachine/manifest.json | 2 +- .../components/rainmachine/switch.py | 37 +++++++------- mypy.ini | 14 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/mypy_config.py | 1 - 9 files changed, 83 insertions(+), 61 deletions(-) diff --git a/.strict-typing b/.strict-typing index 82069f1548a..d3ff7fb42f0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -74,6 +74,7 @@ homeassistant.components.openuv.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.proximity.* +homeassistant.components.rainmachine.* homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 13540c092fa..8d3f9444f08 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -1,7 +1,10 @@ """Support for RainMachine devices.""" +from __future__ import annotations + import asyncio from datetime import timedelta from functools import partial +from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller @@ -93,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.entry_id ] = get_client_controller(client) - entry_updates = {} + entry_updates: dict[str, Any] = {} if not entry.unique_id or is_ip_address(entry.unique_id): # If the config entry doesn't already have a unique ID, set one: entry_updates["unique_id"] = controller.mac @@ -111,23 +114,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update(api_category: str) -> dict: """Update the appropriate API data based on a category.""" + data: dict = {} + try: if api_category == DATA_PROGRAMS: - return await controller.programs.all(include_inactive=True) - - if api_category == DATA_PROVISION_SETTINGS: - return await controller.provisioning.settings() - - if api_category == DATA_RESTRICTIONS_CURRENT: - return await controller.restrictions.current() - - if api_category == DATA_RESTRICTIONS_UNIVERSAL: - return await controller.restrictions.universal() - - return await controller.zones.all(details=True, include_inactive=True) + data = await controller.programs.all(include_inactive=True) + elif api_category == DATA_PROVISION_SETTINGS: + data = await controller.provisioning.settings() + elif api_category == DATA_RESTRICTIONS_CURRENT: + data = await controller.restrictions.current() + elif api_category == DATA_RESTRICTIONS_UNIVERSAL: + data = await controller.restrictions.universal() + else: + data = await controller.zones.all(details=True, include_inactive=True) except RainMachineError as err: raise UpdateFailed(err) from err + return data + controller_init_tasks = [] for api_category in ( DATA_PROGRAMS, @@ -201,12 +205,12 @@ class RainMachineEntity(CoordinatorEntity): self._entity_type = entity_type @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" self.update_from_latest_data() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() self.update_from_latest_data() diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 55ff68c5ea0..c392ad1f8ce 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -1,31 +1,33 @@ """Config flow to configure the RainMachine component.""" +from __future__ import annotations + +from typing import Any + from regenmaschine import Client +from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_IP_ADDRESS): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - } -) - -def get_client_controller(client): +@callback +def get_client_controller(client: Client) -> Controller: """Return the first local controller.""" return next(iter(client.controllers.values())) -async def async_get_controller(hass, ip_address, password, port, ssl): +async def async_get_controller( + hass: HomeAssistant, ip_address: str, password: str, port: int, ssl: bool +) -> Controller | None: """Auth and fetch the mac address from the controller.""" websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -42,21 +44,23 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): - """Initialize config flow.""" - self.discovered_ip_address = None + discovered_ip_address: str | None = None @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> RainMachineOptionsFlowHandler: """Define the config flow to handle options.""" return RainMachineOptionsFlowHandler(config_entry) - async def async_step_homekit(self, discovery_info): + async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by homekit discovery.""" return await self.async_step_zeroconf(discovery_info) - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle discovery via zeroconf.""" ip_address = discovery_info["host"] @@ -86,7 +90,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() @callback - def _async_generate_schema(self): + def _async_generate_schema(self) -> vol.Schema: """Generate schema.""" return vol.Schema( { @@ -96,7 +100,9 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" errors = {} if user_input: @@ -134,6 +140,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.discovered_ip_address: self.context["title_placeholders"] = {"ip": self.discovered_ip_address} + return self.async_show_form( step_id="user", data_schema=self._async_generate_schema(), errors=errors ) @@ -142,11 +149,13 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class RainMachineOptionsFlowHandler(config_entries.OptionsFlow): """Handle a RainMachine options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b6021d02c39..03fedcf8c57 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==3.0.0"], + "requirements": ["regenmaschine==3.1.5"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 8338e4c8305..9554b22d783 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Coroutine from datetime import datetime +from typing import Any from regenmaschine.controller import Controller from regenmaschine.errors import RequestError @@ -165,7 +166,8 @@ async def async_setup_entry( ] zones_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][DATA_ZONES] - entities = [] + entities: list[RainMachineProgram | RainMachineZone] = [] + for uid, program in programs_coordinator.data.items(): entities.append( RainMachineProgram( @@ -241,57 +243,57 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): async_update_programs_and_zones(self.hass, self._entry) ) - async def async_disable_program(self, *, program_id): + async def async_disable_program(self, *, program_id: int) -> None: """Disable a program.""" await self._controller.programs.disable(program_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_disable_zone(self, *, zone_id): + async def async_disable_zone(self, *, zone_id: int) -> None: """Disable a zone.""" await self._controller.zones.disable(zone_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_enable_program(self, *, program_id): + async def async_enable_program(self, *, program_id: int) -> None: """Enable a program.""" await self._controller.programs.enable(program_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_enable_zone(self, *, zone_id): + async def async_enable_zone(self, *, zone_id: int) -> None: """Enable a zone.""" await self._controller.zones.enable(zone_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_pause_watering(self, *, seconds): + async def async_pause_watering(self, *, seconds: int) -> None: """Pause watering for a set number of seconds.""" await self._controller.watering.pause_all(seconds) await async_update_programs_and_zones(self.hass, self._entry) - async def async_start_program(self, *, program_id): + async def async_start_program(self, *, program_id: int) -> None: """Start a particular program.""" await self._controller.programs.start(program_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_start_zone(self, *, zone_id, zone_run_time): + async def async_start_zone(self, *, zone_id: int, zone_run_time: int) -> None: """Start a particular zone for a certain amount of time.""" await self._controller.zones.start(zone_id, zone_run_time) await async_update_programs_and_zones(self.hass, self._entry) - async def async_stop_all(self): + async def async_stop_all(self) -> None: """Stop all watering.""" await self._controller.watering.stop_all() await async_update_programs_and_zones(self.hass, self._entry) - async def async_stop_program(self, *, program_id): + async def async_stop_program(self, *, program_id: int) -> None: """Stop a program.""" await self._controller.programs.stop(program_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_stop_zone(self, *, zone_id): + async def async_stop_zone(self, *, zone_id: int) -> None: """Stop a zone.""" await self._controller.zones.stop(zone_id) await async_update_programs_and_zones(self.hass, self._entry) - async def async_unpause_watering(self): + async def async_unpause_watering(self) -> None: """Unpause watering.""" await self._controller.watering.unpause_all() await async_update_programs_and_zones(self.hass, self._entry) @@ -311,13 +313,13 @@ class RainMachineProgram(RainMachineSwitch): """Return a list of active zones associated with this program.""" return [z for z in self._data["wateringTimes"] if z["active"]] - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: dict[str, Any]) -> None: """Turn the program off.""" await self._async_run_switch_coroutine( self._controller.programs.stop(self._uid) ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: dict[str, Any]) -> None: """Turn the program on.""" await self._async_run_switch_coroutine( self._controller.programs.start(self._uid) @@ -330,13 +332,12 @@ class RainMachineProgram(RainMachineSwitch): self._attr_is_on = bool(self._data["status"]) + next_run: str | None = None if self._data.get("nextRun") is not None: next_run = datetime.strptime( f"{self._data['nextRun']} {self._data['startTime']}", "%Y-%m-%d %H:%M", ).isoformat() - else: - next_run = None self._attr_extra_state_attributes.update( { @@ -352,11 +353,11 @@ class RainMachineProgram(RainMachineSwitch): class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: dict[str, Any]) -> None: """Turn the zone off.""" await self._async_run_switch_coroutine(self._controller.zones.stop(self._uid)) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: dict[str, Any]) -> None: """Turn the zone on.""" await self._async_run_switch_coroutine( self._controller.zones.start( diff --git a/mypy.ini b/mypy.ini index 072242f44e7..46a08049bfd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -825,6 +825,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rainmachine.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.purge] check_untyped_defs = true disallow_incomplete_defs = true @@ -1535,9 +1546,6 @@ ignore_errors = true [mypy-homeassistant.components.rachio.*] ignore_errors = true -[mypy-homeassistant.components.rainmachine.*] -ignore_errors = true - [mypy-homeassistant.components.recollect_waste.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index d9c8162d26d..71e2da64aa2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2010,7 +2010,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==3.0.0 +regenmaschine==3.1.5 # homeassistant.components.python_script restrictedpython==5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6704cb73b5c..5f3042643ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1107,7 +1107,7 @@ pyzerproc==0.4.8 rachiopy==1.0.3 # homeassistant.components.rainmachine -regenmaschine==3.0.0 +regenmaschine==3.1.5 # homeassistant.components.python_script restrictedpython==5.1 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 642dd47b732..f07575b61de 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -137,7 +137,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.profiler.*", "homeassistant.components.proxmoxve.*", "homeassistant.components.rachio.*", - "homeassistant.components.rainmachine.*", "homeassistant.components.recollect_waste.*", "homeassistant.components.reddit.*", "homeassistant.components.ring.*", From 18bf0762b551d2469ba63aaa0e2c2aa6f1dbc2db Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 27 Jul 2021 21:45:04 +1200 Subject: [PATCH 620/818] Add select entities to ESPHome (#53526) Co-authored-by: Otto Winter --- .coveragerc | 1 + .../components/esphome/entry_data.py | 2 + .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/select.py | 65 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/esphome/select.py diff --git a/.coveragerc b/.coveragerc index 759e2251988..9a89bf842c6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -279,6 +279,7 @@ omit = homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py homeassistant/components/esphome/number.py + homeassistant/components/esphome/select.py homeassistant/components/esphome/sensor.py homeassistant/components/esphome/switch.py homeassistant/components/essent/sensor.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 3ab933f75f9..2b926b9b270 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -19,6 +19,7 @@ from aioesphomeapi import ( FanInfo, LightInfo, NumberInfo, + SelectInfo, SensorInfo, SwitchInfo, TextSensorInfo, @@ -41,6 +42,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { FanInfo: "fan", LightInfo: "light", NumberInfo: "number", + SelectInfo: "select", SensorInfo: "sensor", SwitchInfo: "switch", TextSensorInfo: "sensor", diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d8a22534001..218c349e5a8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==5.0.1"], + "requirements": ["aioesphomeapi==5.1.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py new file mode 100644 index 00000000000..6ba6ba4c594 --- /dev/null +++ b/homeassistant/components/esphome/select.py @@ -0,0 +1,65 @@ +"""Support for esphome selects.""" +from __future__ import annotations + +from typing import cast + +from aioesphomeapi import SelectInfo, SelectState +import voluptuous as vol + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry + +ICON_SCHEMA = vol.Schema(cv.icon) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up esphome selects based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="select", + info_type=SelectInfo, + entity_type=EsphomeSelect, + state_type=SelectState, + ) + + +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + +class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): + """A select implementation for esphome.""" + + @property + def icon(self) -> str | None: + """Return the icon.""" + if not self._static_info.icon: + return None + return cast(str, ICON_SCHEMA(self._static_info.icon)) + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + return self._static_info.options + + @esphome_state_property + def current_option(self) -> str | None: + """Return the state of the entity.""" + if self._state.missing_state: + return None + return self._state.state + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self._client.select_command(self._static_info.key, option) diff --git a/requirements_all.txt b/requirements_all.txt index 71e2da64aa2..7ee67487da6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.0.1 +aioesphomeapi==5.1.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f3042643ce..9569a63d0c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.0.1 +aioesphomeapi==5.1.1 # homeassistant.components.flo aioflo==0.4.1 From 7103835d156bc0cfbbc3163216fa98646fd4e0d7 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 11:50:47 +0200 Subject: [PATCH 621/818] Enable strict typing for Rituals Perfume Genie (#53543) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + .../components/rituals_perfume_genie/__init__.py | 2 +- .../components/rituals_perfume_genie/config_flow.py | 7 ++++++- mypy.ini | 11 +++++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index d3ff7fb42f0..2a1f3bc8cc7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -79,6 +79,7 @@ homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics homeassistant.components.remote.* +homeassistant.components.rituals_perfume_genie.* homeassistant.components.scene.* homeassistant.components.select.* homeassistant.components.sensor.* diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 26dbfca08d9..ee2a517a3f7 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 294dced4217..f9a7f1cb6b8 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -1,5 +1,8 @@ """Config flow for Rituals Perfume Genie integration.""" +from __future__ import annotations + import logging +from typing import Any from aiohttp import ClientResponseError from pyrituals import Account, AuthenticationException @@ -27,7 +30,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/mypy.ini b/mypy.ini index 46a08049bfd..644a8dc22e6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -880,6 +880,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rituals_perfume_genie.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.scene.*] check_untyped_defs = true disallow_incomplete_defs = true From f4a7292f08c89c41b4f6c335dbc909bb9fe13499 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 27 Jul 2021 03:51:57 -0600 Subject: [PATCH 622/818] Enforce strict typing for Tile (#53410) --- .strict-typing | 1 + homeassistant/components/tile/__init__.py | 20 ++++--- homeassistant/components/tile/config_flow.py | 15 +++-- .../components/tile/device_tracker.py | 56 ++++++++++++++----- homeassistant/components/tile/manifest.json | 2 +- mypy.ini | 11 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 79 insertions(+), 30 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2a1f3bc8cc7..24d35370ca5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,6 +95,7 @@ homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* homeassistant.components.tag.* homeassistant.components.tcp.* +homeassistant.components.tile.* homeassistant.components.tts.* homeassistant.components.upcloud.* homeassistant.components.uptime.* diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index d8a592a2f09..5b52e637c64 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,15 +1,19 @@ """The Tile component.""" +from __future__ import annotations + from datetime import timedelta from functools import partial from pytile import async_login from pytile.errors import InvalidAuthError, SessionExpiredError, TileError +from pytile.tile import Tile +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity_registry import async_migrate_entries +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.async_ import gather_with_concurrency @@ -24,14 +28,14 @@ DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tile as config entry.""" hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_TILE: {}}) hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} @callback - def async_migrate_callback(entity_entry): + def async_migrate_callback(entity_entry: RegistryEntry) -> dict | None: """ Define a callback to migrate appropriate Tile entities to new unique IDs. @@ -39,7 +43,7 @@ async def async_setup_entry(hass, entry): New: {username}_{uuid} """ if entity_entry.unique_id.startswith(entry.data[CONF_USERNAME]): - return + return None new_unique_id = f"{entry.data[CONF_USERNAME]}_".join( entity_entry.unique_id.split(f"{DOMAIN}_") @@ -71,10 +75,10 @@ async def async_setup_entry(hass, entry): except TileError as err: raise ConfigEntryNotReady("Error during integration setup") from err - async def async_update_tile(tile): + async def async_update_tile(tile: Tile) -> None: """Update the Tile.""" try: - return await tile.async_update() + await tile.async_update() except SessionExpiredError: LOGGER.info("Tile session expired; creating a new one") await client.async_init() @@ -101,7 +105,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a Tile config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 23bf4ffa79b..3c78e5d2bca 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -1,10 +1,15 @@ """Config flow to configure the Tile integration.""" +from __future__ import annotations + +from typing import Any + from pytile import async_login from pytile.errors import TileError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -15,23 +20,25 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" self.data_schema = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) - async def _show_form(self, errors=None): + async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", data_schema=self.data_schema, errors=errors or {} ) - async def async_step_import(self, import_config): + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return await self._show_form() diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index ea1b853d9ae..27446389f50 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,12 +1,23 @@ """Support for Tile device trackers.""" +from __future__ import annotations + +from collections.abc import Awaitable import logging +from typing import Any, Callable + +from pytile.tile import Tile from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import DATA_COORDINATOR, DATA_TILE, DOMAIN @@ -25,7 +36,9 @@ DEFAULT_ATTRIBUTION = "Data provided by Tile" DEFAULT_ICON = "mdi:view-grid" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Tile device trackers.""" async_add_entities( [ @@ -39,7 +52,12 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner( + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: dict[str, Any] | None = None, +) -> bool: """Detect a legacy configuration and import it.""" hass.async_create_task( hass.config_entries.flow.async_init( @@ -65,7 +83,9 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): _attr_icon = DEFAULT_ICON - def __init__(self, entry, coordinator, tile): + def __init__( + self, entry: ConfigEntry, coordinator: DataUpdateCoordinator, tile: Tile + ) -> None: """Initialize.""" super().__init__(coordinator) @@ -76,41 +96,47 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): self._tile = tile @property - def available(self): + def available(self) -> bool: """Return if entity is available.""" return super().available and not self._tile.dead @property - def location_accuracy(self): + def location_accuracy(self) -> int: """Return the location accuracy of the device. Value in meters. """ - return self._tile.accuracy + if not self._tile.accuracy: + return super().location_accuracy + return int(self._tile.accuracy) @property - def latitude(self) -> float: + def latitude(self) -> float | None: """Return latitude value of the device.""" + if not self._tile.latitude: + return None return self._tile.latitude @property - def longitude(self) -> float: + def longitude(self) -> float | None: """Return longitude value of the device.""" + if not self._tile.longitude: + return None return self._tile.longitude @property - def source_type(self): + def source_type(self) -> str: """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" self._update_from_latest_data() self.async_write_ha_state() @callback - def _update_from_latest_data(self): + def _update_from_latest_data(self) -> None: """Update the entity from the latest data.""" self._attr_extra_state_attributes.update( { @@ -122,7 +148,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): } ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() self._update_from_latest_data() diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index e8d386f4a88..39295eed646 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.2.2"], + "requirements": ["pytile==5.2.3"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/mypy.ini b/mypy.ini index 644a8dc22e6..2342bf3d966 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1056,6 +1056,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tile.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.tts.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 7ee67487da6..dfbd6090ffb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1928,7 +1928,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.2.2 +pytile==5.2.3 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9569a63d0c3..63caf5e42d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1071,7 +1071,7 @@ python-velbus==2.1.2 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.2.2 +pytile==5.2.3 # homeassistant.components.traccar pytraccar==0.9.0 From 4506b41022ac15cd37d7c124386caf338fcd1bbc Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 27 Jul 2021 03:05:21 -0700 Subject: [PATCH 623/818] Use SensorEntityDescription for wemo (#53537) --- homeassistant/components/wemo/sensor.py | 41 +++++++++++++++---------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 96856f0a140..ebd68231e0c 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -3,7 +3,11 @@ import asyncio from datetime import datetime, timedelta from typing import Callable -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -51,9 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class InsightSensor(WemoSubscriptionEntity, SensorEntity): """Common base for WeMo Insight power sensors.""" - _attr_state_class = STATE_CLASS_MEASUREMENT _name_suffix: str - _insight_params_key: str def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None: """Initialize the WeMo Insight power sensor.""" @@ -63,18 +65,19 @@ class InsightSensor(WemoSubscriptionEntity, SensorEntity): @property def name(self) -> str: """Return the name of the entity if any.""" - return f"{self.wemo.name} {self._name_suffix}" + return f"{super().name} {self.entity_description.name}" @property def unique_id(self) -> str: """Return the id of this entity.""" - return f"{self.wemo.serialnumber}_{self._insight_params_key}" + return f"{super().unique_id}_{self.entity_description.key}" @property def available(self) -> str: """Return true if sensor is available.""" return ( - self._insight_params_key in self.wemo.insight_params and super().available + self.entity_description.key in self.wemo.insight_params + and super().available ) def _update(self, force_update=True) -> None: @@ -86,16 +89,19 @@ class InsightSensor(WemoSubscriptionEntity, SensorEntity): class InsightCurrentPower(InsightSensor): """Current instantaineous power consumption.""" - _attr_device_class = DEVICE_CLASS_POWER - _attr_unit_of_measurement = POWER_WATT - _name_suffix = "Current Power" - _insight_params_key = "currentpower" + entity_description = SensorEntityDescription( + key="currentpower", + name="Current Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=POWER_WATT, + ) @property def state(self) -> StateType: """Return the current power consumption.""" return ( - convert(self.wemo.insight_params[self._insight_params_key], float, 0.0) + convert(self.wemo.insight_params[self.entity_description.key], float, 0.0) / 1000.0 ) @@ -103,10 +109,13 @@ class InsightCurrentPower(InsightSensor): class InsightTodayEnergy(InsightSensor): """Energy used today.""" - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _name_suffix = "Today Energy" - _insight_params_key = "todaymw" + entity_description = SensorEntityDescription( + key="todaymw", + name="Today Energy", + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ) @property def last_reset(self) -> datetime: @@ -117,6 +126,6 @@ class InsightTodayEnergy(InsightSensor): def state(self) -> StateType: """Return the current energy use today.""" miliwatts = convert( - self.wemo.insight_params[self._insight_params_key], float, 0.0 + self.wemo.insight_params[self.entity_description.key], float, 0.0 ) return round(miliwatts / (1000.0 * 1000.0 * 60), 2) From c0e84a7b3227368549f5fbfb64cfa30358c296dc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 27 Jul 2021 12:08:31 +0200 Subject: [PATCH 624/818] Use SensorEntityDescription in Airly integration (#53540) --- homeassistant/components/airly/const.py | 109 +++++++++++------------ homeassistant/components/airly/model.py | 17 ++-- homeassistant/components/airly/sensor.py | 70 ++++++--------- 3 files changed, 88 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index d79a33a66ab..157f28c33f7 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -3,10 +3,8 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -16,7 +14,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) -from .model import SensorDescription +from .model import AirlySensorEntityDescription ATTR_API_ADVICE: Final = "ADVICE" ATTR_API_CAQI: Final = "CAQI" @@ -31,12 +29,9 @@ ATTR_API_TEMPERATURE: Final = "TEMPERATURE" ATTR_ADVICE: Final = "advice" ATTR_DESCRIPTION: Final = "description" -ATTR_LABEL: Final = "label" ATTR_LEVEL: Final = "level" ATTR_LIMIT: Final = "limit" ATTR_PERCENT: Final = "percent" -ATTR_UNIT: Final = "unit" -ATTR_VALUE: Final = "value" SUFFIX_PERCENT: Final = "PERCENT" SUFFIX_LIMIT: Final = "LIMIT" @@ -51,52 +46,54 @@ MAX_UPDATE_INTERVAL: Final = 90 MIN_UPDATE_INTERVAL: Final = 5 NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." -SENSOR_TYPES: dict[str, SensorDescription] = { - ATTR_API_CAQI: { - ATTR_LABEL: ATTR_API_CAQI, - ATTR_UNIT: "CAQI", - ATTR_VALUE: round, - }, - ATTR_API_PM1: { - ATTR_ICON: "mdi:blur", - ATTR_LABEL: ATTR_API_PM1, - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_API_PM25: { - ATTR_ICON: "mdi:blur", - ATTR_LABEL: "PM2.5", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_API_PM10: { - ATTR_ICON: "mdi:blur", - ATTR_LABEL: ATTR_API_PM10, - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_API_HUMIDITY: { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), - ATTR_UNIT: PERCENTAGE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: lambda value: round(value, 1), - }, - ATTR_API_PRESSURE: { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), - ATTR_UNIT: PRESSURE_HPA, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_API_TEMPERATURE: { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), - ATTR_UNIT: TEMP_CELSIUS, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: lambda value: round(value, 1), - }, -} +SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( + AirlySensorEntityDescription( + key=ATTR_API_CAQI, + name=ATTR_API_CAQI, + unit_of_measurement="CAQI", + ), + AirlySensorEntityDescription( + key=ATTR_API_PM1, + icon="mdi:blur", + name=ATTR_API_PM1, + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_PM25, + icon="mdi:blur", + name="PM2.5", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_PM10, + icon="mdi:blur", + name=ATTR_API_PM10, + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_HUMIDITY, + device_class=DEVICE_CLASS_HUMIDITY, + name=ATTR_API_HUMIDITY.capitalize(), + unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + value=lambda value: round(value, 1), + ), + AirlySensorEntityDescription( + key=ATTR_API_PRESSURE, + device_class=DEVICE_CLASS_PRESSURE, + name=ATTR_API_PRESSURE.capitalize(), + unit_of_measurement=PRESSURE_HPA, + state_class=STATE_CLASS_MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_TEMPERATURE, + device_class=DEVICE_CLASS_TEMPERATURE, + name=ATTR_API_TEMPERATURE.capitalize(), + unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + value=lambda value: round(value, 1), + ), +) diff --git a/homeassistant/components/airly/model.py b/homeassistant/components/airly/model.py index fe8ad6c929b..38b433de34c 100644 --- a/homeassistant/components/airly/model.py +++ b/homeassistant/components/airly/model.py @@ -1,15 +1,14 @@ """Type definitions for Airly integration.""" from __future__ import annotations -from typing import Callable, TypedDict +from dataclasses import dataclass +from typing import Callable + +from homeassistant.components.sensor import SensorEntityDescription -class SensorDescription(TypedDict, total=False): - """Sensor description class.""" +@dataclass +class AirlySensorEntityDescription(SensorEntityDescription): + """Class describing Airly sensor entities.""" - device_class: str | None - icon: str | None - label: str - unit: str - state_class: str | None - value: Callable + value: Callable = round diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 3f9048dd03e..2c811b00aa6 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -3,16 +3,10 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_ICON, - CONF_NAME, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -27,12 +21,9 @@ from .const import ( ATTR_API_PM10, ATTR_API_PM25, ATTR_DESCRIPTION, - ATTR_LABEL, ATTR_LEVEL, ATTR_LIMIT, ATTR_PERCENT, - ATTR_UNIT, - ATTR_VALUE, ATTRIBUTION, DEFAULT_NAME, DOMAIN, @@ -41,6 +32,7 @@ from .const import ( SUFFIX_LIMIT, SUFFIX_PERCENT, ) +from .model import AirlySensorEntityDescription PARALLEL_UPDATES = 1 @@ -54,10 +46,10 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id] sensors = [] - for sensor in SENSOR_TYPES: + for description in SENSOR_TYPES: # When we use the nearest method, we are not sure which sensors are available - if coordinator.data.get(sensor): - sensors.append(AirlySensor(coordinator, name, sensor)) + if coordinator.data.get(description.key): + sensors.append(AirlySensor(coordinator, name, description)) async_add_entities(sensors, False) @@ -66,47 +58,54 @@ class AirlySensor(CoordinatorEntity, SensorEntity): """Define an Airly sensor.""" coordinator: AirlyDataUpdateCoordinator + entity_description: AirlySensorEntityDescription def __init__( - self, coordinator: AirlyDataUpdateCoordinator, name: str, kind: str + self, + coordinator: AirlyDataUpdateCoordinator, + name: str, + description: AirlySensorEntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) - self._description = description = SENSOR_TYPES[kind] - self._attr_device_class = description.get(ATTR_DEVICE_CLASS) - self._attr_icon = description.get(ATTR_ICON) - self._attr_name = f"{name} {description[ATTR_LABEL]}" - self._attr_state_class = description.get(ATTR_STATE_CLASS) + self._attr_device_info = { + "identifiers": { + (DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}") + }, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( - f"{coordinator.latitude}-{coordinator.longitude}-{kind.lower()}" + f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() ) - self._attr_unit_of_measurement = description.get(ATTR_UNIT) self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} - self.kind = kind + self.entity_description = description @property def state(self) -> StateType: """Return the state.""" - state = self.coordinator.data[self.kind] - return cast(StateType, self._description[ATTR_VALUE](state)) + state = self.coordinator.data[self.entity_description.key] + return cast(StateType, self.entity_description.value(state)) @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.kind == ATTR_API_CAQI: + if self.entity_description.key == ATTR_API_CAQI: self._attrs[ATTR_LEVEL] = self.coordinator.data[ATTR_API_CAQI_LEVEL] self._attrs[ATTR_ADVICE] = self.coordinator.data[ATTR_API_ADVICE] self._attrs[ATTR_DESCRIPTION] = self.coordinator.data[ ATTR_API_CAQI_DESCRIPTION ] - if self.kind == ATTR_API_PM25: + if self.entity_description.key == ATTR_API_PM25: self._attrs[ATTR_LIMIT] = self.coordinator.data[ f"{ATTR_API_PM25}_{SUFFIX_LIMIT}" ] self._attrs[ATTR_PERCENT] = round( self.coordinator.data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"] ) - if self.kind == ATTR_API_PM10: + if self.entity_description.key == ATTR_API_PM10: self._attrs[ATTR_LIMIT] = self.coordinator.data[ f"{ATTR_API_PM10}_{SUFFIX_LIMIT}" ] @@ -114,18 +113,3 @@ class AirlySensor(CoordinatorEntity, SensorEntity): self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"] ) return self._attrs - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return { - "identifiers": { - ( - DOMAIN, - f"{self.coordinator.latitude}-{self.coordinator.longitude}", - ) - }, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } From ca020e1f875f5e079a01f6458bcd9ff3b01e97ba Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 27 Jul 2021 12:12:15 +0200 Subject: [PATCH 625/818] Use SensorEntityDescription in Nettigo Air Monitor (#53539) --- homeassistant/components/nam/const.py | 317 ++++++++++++------------- homeassistant/components/nam/model.py | 15 -- homeassistant/components/nam/sensor.py | 49 ++-- 3 files changed, 166 insertions(+), 215 deletions(-) delete mode 100644 homeassistant/components/nam/model.py diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index f60d03eea78..85472deba06 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -4,10 +4,11 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2, @@ -22,8 +23,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) -from .model import SensorDescription - SUFFIX_P0: Final = "_p0" SUFFIX_P1: Final = "_p1" SUFFIX_P2: Final = "_p2" @@ -52,10 +51,6 @@ ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" -ATTR_ENABLED: Final = "enabled" -ATTR_LABEL: Final = "label" -ATTR_UNIT: Final = "unit" - DEFAULT_NAME: Final = "Nettigo Air Monitor" DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" @@ -66,165 +61,145 @@ MIGRATION_SENSORS: Final = [ ("humidity", ATTR_DHT22_HUMIDITY), ] -SENSORS: Final[dict[str, SensorDescription]] = { - ATTR_BME280_HUMIDITY: { - ATTR_LABEL: f"{DEFAULT_NAME} BME280 Humidity", - ATTR_UNIT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BME280_PRESSURE: { - ATTR_LABEL: f"{DEFAULT_NAME} BME280 Pressure", - ATTR_UNIT: PRESSURE_HPA, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BME280_TEMPERATURE: { - ATTR_LABEL: f"{DEFAULT_NAME} BME280 Temperature", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BMP280_PRESSURE: { - ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Pressure", - ATTR_UNIT: PRESSURE_HPA, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_BMP280_TEMPERATURE: { - ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Temperature", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_HECA_HUMIDITY: { - ATTR_LABEL: f"{DEFAULT_NAME} HECA Humidity", - ATTR_UNIT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_HECA_TEMPERATURE: { - ATTR_LABEL: f"{DEFAULT_NAME} HECA Temperature", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_MHZ14A_CARBON_DIOXIDE: { - ATTR_LABEL: f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", - ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SDS011_P1: { - ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 10", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SDS011_P2: { - ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SHT3X_HUMIDITY: { - ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity", - ATTR_UNIT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SHT3X_TEMPERATURE: { - ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Temperature", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SPS30_P0: { - ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SPS30_P1: { - ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 10", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SPS30_P2: { - ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SPS30_P4: { - ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_DHT22_HUMIDITY: { - ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Humidity", - ATTR_UNIT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_DHT22_TEMPERATURE: { - ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Temperature", - ATTR_UNIT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_SIGNAL_STRENGTH: { - ATTR_LABEL: f"{DEFAULT_NAME} Signal Strength", - ATTR_UNIT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, - ATTR_ICON: None, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - ATTR_UPTIME: { - ATTR_LABEL: f"{DEFAULT_NAME} Uptime", - ATTR_UNIT: None, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, - ATTR_ICON: None, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: None, - }, -} +SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + key=ATTR_BME280_HUMIDITY, + name=f"{DEFAULT_NAME} BME280 Humidity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_PRESSURE, + name=f"{DEFAULT_NAME} BME280 Pressure", + unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_TEMPERATURE, + name=f"{DEFAULT_NAME} BME280 Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_PRESSURE, + name=f"{DEFAULT_NAME} BMP280 Pressure", + unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_TEMPERATURE, + name=f"{DEFAULT_NAME} BMP280 Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_HUMIDITY, + name=f"{DEFAULT_NAME} HECA Humidity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_TEMPERATURE, + name=f"{DEFAULT_NAME} HECA Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_MHZ14A_CARBON_DIOXIDE, + name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P1, + name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P2, + name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_HUMIDITY, + name=f"{DEFAULT_NAME} SHT3X Humidity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_TEMPERATURE, + name=f"{DEFAULT_NAME} SHT3X Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P0, + name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P1, + name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P2, + name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P4, + name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_HUMIDITY, + name=f"{DEFAULT_NAME} DHT22 Humidity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_TEMPERATURE, + name=f"{DEFAULT_NAME} DHT22 Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SIGNAL_STRENGTH, + name=f"{DEFAULT_NAME} Signal Strength", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_UPTIME, + name=f"{DEFAULT_NAME} Uptime", + device_class=DEVICE_CLASS_TIMESTAMP, + entity_registry_enabled_default=False, + ), +) diff --git a/homeassistant/components/nam/model.py b/homeassistant/components/nam/model.py deleted file mode 100644 index 0cadaad647e..00000000000 --- a/homeassistant/components/nam/model.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Type definitions for Nettig Air Monitor integration.""" -from __future__ import annotations - -from typing import TypedDict - - -class SensorDescription(TypedDict): - """Sensor description class.""" - - label: str - unit: str | None - device_class: str | None - icon: str | None - enabled: bool - state_class: str | None diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index ae3d1e639d5..298f88d5c29 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -6,12 +6,11 @@ import logging from typing import cast from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, DOMAIN as PLATFORM, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -20,15 +19,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator -from .const import ( - ATTR_ENABLED, - ATTR_LABEL, - ATTR_UNIT, - ATTR_UPTIME, - DOMAIN, - MIGRATION_SENSORS, - SENSORS, -) +from .const import ATTR_UPTIME, DOMAIN, MIGRATION_SENSORS, SENSORS PARALLEL_UPDATES = 1 @@ -57,12 +48,12 @@ async def async_setup_entry( ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) sensors: list[NAMSensor | NAMSensorUptime] = [] - for sensor in SENSORS: - if getattr(coordinator.data, sensor) is not None: - if sensor == ATTR_UPTIME: - sensors.append(NAMSensorUptime(coordinator, sensor)) + for description in SENSORS: + if getattr(coordinator.data, description.key) is not None: + if description.key == ATTR_UPTIME: + sensors.append(NAMSensorUptime(coordinator, description)) else: - sensors.append(NAMSensor(coordinator, sensor)) + sensors.append(NAMSensor(coordinator, description)) async_add_entities(sensors, False) @@ -72,24 +63,23 @@ class NAMSensor(CoordinatorEntity, SensorEntity): coordinator: NAMDataUpdateCoordinator - def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: + def __init__( + self, + coordinator: NAMDataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: """Initialize.""" super().__init__(coordinator) - description = SENSORS[sensor_type] - self._attr_device_class = description[ATTR_DEVICE_CLASS] self._attr_device_info = coordinator.device_info - self._attr_entity_registry_enabled_default = description[ATTR_ENABLED] - self._attr_icon = description[ATTR_ICON] - self._attr_name = description[ATTR_LABEL] - self._attr_state_class = description[ATTR_STATE_CLASS] - self._attr_unique_id = f"{coordinator.unique_id}-{sensor_type}" - self._attr_unit_of_measurement = description[ATTR_UNIT] - self.sensor_type = sensor_type + self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" + self.entity_description = description @property def state(self) -> StateType: """Return the state.""" - return cast(StateType, getattr(self.coordinator.data, self.sensor_type)) + return cast( + StateType, getattr(self.coordinator.data, self.entity_description.key) + ) @property def available(self) -> bool: @@ -100,7 +90,8 @@ class NAMSensor(CoordinatorEntity, SensorEntity): # sensors. For this reason, we mark entities for which data is missing as # unavailable. return ( - available and getattr(self.coordinator.data, self.sensor_type) is not None + available + and getattr(self.coordinator.data, self.entity_description.key) is not None ) @@ -110,7 +101,7 @@ class NAMSensorUptime(NAMSensor): @property def state(self) -> str: """Return the state.""" - uptime_sec = getattr(self.coordinator.data, self.sensor_type) + uptime_sec = getattr(self.coordinator.data, self.entity_description.key) return ( (utcnow() - timedelta(seconds=uptime_sec)) .replace(microsecond=0) From 0471b2717907a42916a50a219bd95df1e186fbc7 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 12:30:56 +0200 Subject: [PATCH 626/818] Replace HomeAssistantType with HomeAssistant (#53545) --- homeassistant/components/hyperion/camera.py | 5 ++--- homeassistant/components/lcn/__init__.py | 9 +++++---- homeassistant/components/lcn/binary_sensor.py | 7 ++++--- homeassistant/components/lcn/climate.py | 7 ++++--- homeassistant/components/lcn/config_flow.py | 5 +++-- homeassistant/components/lcn/cover.py | 7 ++++--- homeassistant/components/lcn/helpers.py | 5 +++-- homeassistant/components/lcn/light.py | 7 ++++--- homeassistant/components/lcn/scene.py | 7 ++++--- homeassistant/components/lcn/sensor.py | 7 ++++--- homeassistant/components/lcn/services.py | 5 +++-- homeassistant/components/lcn/switch.py | 7 ++++--- homeassistant/components/meteoclimatic/sensor.py | 4 ++-- homeassistant/components/modern_forms/fan.py | 4 ++-- homeassistant/components/modern_forms/light.py | 4 ++-- homeassistant/components/philips_js/light.py | 5 ++--- homeassistant/components/siren/__init__.py | 10 +++++----- .../components/yamaha_musiccast/media_player.py | 8 ++++---- 18 files changed, 61 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 1ef88969228..22134400a45 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -27,14 +27,13 @@ from homeassistant.components.camera import ( async_get_still_stream, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from . import ( get_hyperion_device_id, @@ -57,7 +56,7 @@ IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64," async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 7a6a7c5fab6..9db564812a8 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -15,9 +15,10 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN, PLATFORMS from .helpers import ( @@ -32,7 +33,7 @@ from .services import SERVICES _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LCN component.""" if DOMAIN not in config: return True @@ -53,7 +54,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry( - hass: HomeAssistantType, config_entry: config_entries.ConfigEntry + hass: HomeAssistant, config_entry: config_entries.ConfigEntry ) -> bool: """Set up a connection to PCHK host from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -116,7 +117,7 @@ async def async_setup_entry( async def async_unload_entry( - hass: HomeAssistantType, config_entry: config_entries.ConfigEntry + hass: HomeAssistant, config_entry: config_entries.ConfigEntry ) -> bool: """Close connection to PCHK host represented by config_entry.""" # forward unloading to platforms diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 45bd45c8fd7..13a2a5b3bb3 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -9,8 +9,9 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, SETPOINTS @@ -18,7 +19,7 @@ from .helpers import DeviceConnectionType, InputType, get_device_connection def create_lcn_binary_sensor_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -36,7 +37,7 @@ def create_lcn_binary_sensor_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 3bd8b551c49..4254a5e5480 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -21,8 +21,9 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import ( @@ -38,7 +39,7 @@ PARALLEL_UPDATES = 0 def create_lcn_climate_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -49,7 +50,7 @@ def create_lcn_climate_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 9ac8cc7f1fa..905da4d005c 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -13,8 +13,9 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN @@ -22,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) def get_config_entry( - hass: HomeAssistantType, data: ConfigType + hass: HomeAssistant, data: ConfigType ) -> config_entries.ConfigEntry | None: """Check config entries for already configured entries based on the ip address/port.""" return next( diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 608881435dc..bc83da55888 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -8,8 +8,9 @@ import pypck from homeassistant.components.cover import DOMAIN as DOMAIN_COVER, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import CONF_DOMAIN_DATA, CONF_MOTOR, CONF_REVERSE_TIME @@ -19,7 +20,7 @@ PARALLEL_UPDATES = 0 def create_lcn_cover_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -33,7 +34,7 @@ def create_lcn_cover_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 07e3866cd48..53026d0294c 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -25,7 +25,8 @@ from homeassistant.const import ( CONF_SWITCHES, CONF_USERNAME, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_CLIMATES, @@ -68,7 +69,7 @@ DOMAIN_LOOKUP = { def get_device_connection( - hass: HomeAssistantType, address: AddressType, config_entry: ConfigEntry + hass: HomeAssistant, address: AddressType, config_entry: ConfigEntry ) -> DeviceConnectionType | None: """Return a lcn device_connection.""" host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 2a2d050143c..260dd2212ea 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -15,8 +15,9 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import ( @@ -32,7 +33,7 @@ PARALLEL_UPDATES = 0 def create_lcn_light_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -46,7 +47,7 @@ def create_lcn_light_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index d3faa887dc8..f1980b6475d 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -8,8 +8,9 @@ import pypck from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import ( @@ -25,7 +26,7 @@ PARALLEL_UPDATES = 0 def create_lcn_scene_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -36,7 +37,7 @@ def create_lcn_scene_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index a1451f339f2..fdd6ee51872 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -14,8 +14,9 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import ( @@ -30,7 +31,7 @@ from .helpers import DeviceConnectionType, InputType, get_device_connection def create_lcn_sensor_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -49,7 +50,7 @@ def create_lcn_sensor_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 7df6f4ce2ed..47efc3ea060 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -11,8 +11,9 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, TIME_SECONDS, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ServiceCallType from .const import ( CONF_KEYS, @@ -54,7 +55,7 @@ class LcnServiceCall: schema = vol.Schema({vol.Required(CONF_ADDRESS): is_address}) - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize service call.""" self.hass = hass diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index f5159b3492d..ded15c0f1da 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -8,8 +8,9 @@ import pypck from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import LcnEntity from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS @@ -19,7 +20,7 @@ PARALLEL_UPDATES = 0 def create_lcn_switch_entity( - hass: HomeAssistantType, entity_config: ConfigType, config_entry: ConfigEntry + hass: HomeAssistant, entity_config: ConfigType, config_entry: ConfigEntry ) -> LcnEntity: """Set up an entity for this domain.""" device_connection = get_device_connection( @@ -33,7 +34,7 @@ def create_lcn_switch_entity( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index bcd597d4b0c..101b889498d 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Meteoclimatic sensor platform.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index db8f2e011a9..7c1da064b18 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -8,9 +8,9 @@ import voluptuous as vol from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -34,7 +34,7 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index ee30c0c489f..7431cf0c3bb 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -12,9 +12,9 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -39,7 +39,7 @@ BRIGHTNESS_RANGE = (1, 255) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 4c321468d79..3dbca7611ab 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -18,8 +18,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, LightEntity, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import color_hsv_to_RGB, color_RGB_to_hsv @@ -34,7 +33,7 @@ EFFECT_EXPERT_STYLES = {"FOLLOW_AUDIO", "FOLLOW_COLOR", "Lounge light"} async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities, ): diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 565e8ca0b7a..f301100fa6c 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.core import ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_AVAILABLE_TONES, @@ -77,7 +77,7 @@ def process_turn_on_params( return params -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up siren devices.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -110,13 +110,13 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" component: EntityComponent = hass.data[DOMAIN] return await component.async_setup_entry(entry) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" component: EntityComponent = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 77bc7a21b85..d08ba798bd8 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -34,12 +34,12 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import DiscoveryInfoType, HomeAssistantType +from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import uuid from . import MusicCastDataUpdateCoordinator, MusicCastDeviceEntity @@ -79,7 +79,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config, async_add_devices: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, @@ -107,7 +107,7 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: From 348d7a5622e8d3f71adbb6272d6ad7442fc6f92e Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 12:33:17 +0200 Subject: [PATCH 627/818] Remove incorrect use of ConfigType in config flows (#53544) --- .../components/bsblan/config_flow.py | 6 +++-- .../components/canary/config_flow.py | 12 ++++++---- .../components/directv/config_flow.py | 8 ++++--- .../components/esphome/config_flow.py | 16 +++++++------ .../components/hyperion/config_flow.py | 17 +++++++------- .../components/iaqualink/config_flow.py | 4 +++- homeassistant/components/ipp/config_flow.py | 9 ++++---- .../components/keenetic_ndms2/config_flow.py | 13 ++++++++--- .../components/nzbget/config_flow.py | 6 +++-- .../components/plum_lightpad/config_flow.py | 5 +++- homeassistant/components/sia/config_flow.py | 23 +++++++++++-------- .../components/sonarr/config_flow.py | 11 +++++---- .../components/system_bridge/config_flow.py | 4 ++-- .../yamaha_musiccast/config_flow.py | 4 ++-- 14 files changed, 83 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 88866b27801..dff77739106 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from bsblan import BSBLan, BSBLanError, Info import voluptuous as vol @@ -10,7 +11,6 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import CONF_DEVICE_IDENT, CONF_PASSKEY, DOMAIN @@ -22,7 +22,9 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index ac779a9cb69..967273a0f34 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Final +from typing import Any, Final from canary.api import Api from requests.exceptions import ConnectTimeout, HTTPError @@ -24,7 +24,7 @@ from .const import ( _LOGGER: Final = logging.getLogger(__name__) -def validate_input(hass: HomeAssistant, data: ConfigType) -> bool: +def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -56,7 +56,9 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -104,7 +106,9 @@ class CanaryOptionsFlowHandler(OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage Canary options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 325dbb195e9..d2d1e2ec003 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_RECEIVER_ID, DOMAIN @@ -45,7 +45,9 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): """Set up the instance.""" self.discovery_info = {} - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -97,7 +99,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_ssdp_confirm() async def async_step_ssdp_confirm( - self, user_input: ConfigType = None + self, user_input: dict[str, Any] = None ) -> FlowResult: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 3062b9690bf..247484ba317 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import DiscoveryInfoType from . import DOMAIN, DomainData @@ -29,7 +29,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._password: str | None = None async def _async_step_user_base( - self, user_input: ConfigType | None = None, error: str | None = None + self, user_input: dict[str, Any] | None = None, error: str | None = None ) -> FlowResult: if user_input is not None: return await self._async_authenticate_or_add(user_input) @@ -46,7 +46,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" return await self._async_step_user_base(user_input=user_input) @@ -59,14 +61,14 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} - def _set_user_input(self, user_input: ConfigType | None) -> None: + def _set_user_input(self, user_input: dict[str, Any] | None) -> None: if user_input is None: return self._host = user_input[CONF_HOST] self._port = user_input[CONF_PORT] async def _async_authenticate_or_add( - self, user_input: ConfigType | None + self, user_input: dict[str, Any] | None ) -> FlowResult: self._set_user_input(user_input) error, device_info = await self.fetch_device_info() @@ -82,7 +84,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_get_entry() async def async_step_discovery_confirm( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: @@ -154,7 +156,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) async def async_step_authenticate( - self, user_input: ConfigType | None = None, error: str | None = None + self, user_input: dict[str, Any] | None = None, error: str | None = None ) -> FlowResult: """Handle getting password for authentication.""" if user_input is not None: diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 3e82f796c0e..81fef6429f6 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -28,7 +28,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType from . import create_hyperion_client from .const import ( @@ -143,7 +142,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, - config_data: ConfigType, + config_data: dict[str, Any], ) -> FlowResult: """Handle a reauthentication flow.""" self._data = dict(config_data) @@ -222,7 +221,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, - user_input: ConfigType | None = None, + user_input: dict[str, Any] | None = None, ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -293,7 +292,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, - user_input: ConfigType | None = None, + user_input: dict[str, Any] | None = None, ) -> FlowResult: """Handle the auth step of a flow.""" errors = {} @@ -322,7 +321,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_create_token( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Send a request for a new token.""" if user_input is None: @@ -348,7 +347,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_create_token_external( - self, auth_resp: ConfigType | None = None + self, auth_resp: dict[str, Any] | None = None ) -> FlowResult: """Handle completion of the request for a new token.""" if auth_resp is not None and client.ResponseOK(auth_resp): @@ -361,7 +360,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_external_step_done(next_step_id="create_token_fail") async def async_step_create_token_success( - self, _: ConfigType | None = None + self, _: dict[str, Any] | None = None ) -> FlowResult: """Create an entry after successful token creation.""" # Clean-up the request task. @@ -377,7 +376,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() async def async_step_create_token_fail( - self, _: ConfigType | None = None + self, _: dict[str, Any] | None = None ) -> FlowResult: """Show an error on the auth form.""" # Clean-up the request task. @@ -385,7 +384,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="auth_new_token_not_granted_error") async def async_step_confirm( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 96c82cd2c76..5380a97901e 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure zone component.""" from __future__ import annotations +from typing import Any + from iaqualink import AqualinkClient, AqualinkLoginException import voluptuous as vol @@ -16,7 +18,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: ConfigType | None = None): + async def async_step_user(self, user_input: dict[str, Any] | None = None): """Handle a flow start.""" # Supporting a single account. entries = self._async_current_entries() diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 842bfc91d9a..b760fccb598 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -26,7 +26,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID, DOMAIN @@ -62,7 +61,9 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): """Set up the instance.""" self.discovery_info = {} - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -98,7 +99,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_zeroconf(self, discovery_info: ConfigType) -> FlowResult: + async def async_step_zeroconf(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle zeroconf discovery.""" port = discovery_info[CONF_PORT] zctype = discovery_info["type"] @@ -165,7 +166,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_zeroconf_confirm() async def async_step_zeroconf_confirm( - self, user_input: ConfigType = None + self, user_input: dict[str, Any] = None ) -> FlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index c82524a3410..fdb7dafc516 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Keenetic NDMS2.""" from __future__ import annotations +from typing import Any from urllib.parse import urlparse from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection @@ -50,7 +51,9 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return KeeneticOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -135,7 +138,9 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry self._interface_options = {} - async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][ ROUTER @@ -152,7 +157,9 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): } return await self.async_step_user() - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the device tracker options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index 71419dae641..8aa18502ba3 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -75,7 +75,9 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input) - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -129,7 +131,7 @@ class NZBGetOptionsFlowHandler(OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: ConfigType | None = None): + async def async_step_init(self, user_input: dict[str, Any] | None = None): """Manage NZBGet options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 64c424ae74b..f2cc88538f9 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from aiohttp import ContentTypeError from requests.exceptions import ConnectTimeout, HTTPError @@ -35,7 +36,9 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index a9b49765c19..c43faf5475c 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -19,7 +19,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ACCOUNT, @@ -62,7 +61,7 @@ ACCOUNT_SCHEMA = vol.Schema( DEFAULT_OPTIONS = {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: None} -def validate_input(data: ConfigType) -> dict[str, str] | None: +def validate_input(data: dict[str, Any]) -> dict[str, str] | None: """Validate the input by the user.""" try: SIAAccount.validate_account(data[CONF_ACCOUNT], data.get(CONF_ENCRYPTION_KEY)) @@ -82,7 +81,7 @@ def validate_input(data: ConfigType) -> dict[str, str] | None: return validate_zones(data) -def validate_zones(data: ConfigType) -> dict[str, str] | None: +def validate_zones(data: dict[str, Any]) -> dict[str, str] | None: """Validate the zones field.""" if data[CONF_ZONES] == 0: return {"base": "invalid_zones"} @@ -102,10 +101,10 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self._data: ConfigType = {} + self._data: dict[str, Any] = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: ConfigType = None) -> FlowResult: + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -116,7 +115,9 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_step_add_account(self, user_input: ConfigType = None) -> FlowResult: + async def async_step_add_account( + self, user_input: dict[str, Any] = None + ) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None if user_input is not None: @@ -127,7 +128,9 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_handle_data_and_route(self, user_input: ConfigType) -> FlowResult: + async def async_handle_data_and_route( + self, user_input: dict[str, Any] + ) -> FlowResult: """Handle the user_input, check if configured and route to the right next step or create entry.""" self._update_data(user_input) @@ -141,7 +144,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=self._options, ) - def _update_data(self, user_input: ConfigType) -> None: + def _update_data(self, user_input: dict[str, Any]) -> None: """Parse the user_input and store in data and options attributes. If there is a port in the input or no data, assume it is fully new and overwrite. @@ -175,7 +178,7 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: ConfigType = None) -> FlowResult: + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] assert self.hub is not None @@ -183,7 +186,7 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] return await self.async_step_options() - async def async_step_options(self, user_input: ConfigType = None) -> FlowResult: + async def async_step_options(self, user_input: dict[str, Any] = None) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index b7636ca4db1..db82e729483 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -18,7 +18,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import ( CONF_BASE_PATH, @@ -75,7 +74,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth(self, data: ConfigType | None = None) -> FlowResult: + async def async_step_reauth(self, data: dict[str, Any] | None = None) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True self._entry_data = dict(data) @@ -85,7 +84,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm reauth dialog.""" if user_input is None: @@ -98,7 +97,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -171,7 +172,7 @@ class SonarrOptionsFlowHandler(OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: ConfigType | None = None): + async def async_step_init(self, user_input: dict[str, Any] | None = None): """Manage Sonarr options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index a93420bf6ae..8402a3c1d3e 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import DiscoveryInfoType from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN @@ -167,7 +167,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_authenticate() - async def async_step_reauth(self, entry_data: ConfigType) -> FlowResult: + async def async_step_reauth(self, entry_data: dict[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._name = entry_data[CONF_HOST] self._input = { diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index 06bb212e639..9645be3ddc8 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from urllib.parse import urlparse from aiohttp import ClientConnectorError @@ -13,7 +14,6 @@ from homeassistant.components import ssdp from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -29,7 +29,7 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): host: str async def async_step_user( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> data_entry_flow.FlowResult: """Handle a flow initiated by the user.""" # Request user input, unless we are preparing discovery flow From 93899e981f7726f8e4f6d86afe8f578ef64afd20 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 27 Jul 2021 12:35:16 +0200 Subject: [PATCH 628/818] UniFi lies about the client being noted, using the real note instead if it exists (#53542) --- homeassistant/components/unifi/device_tracker.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 64963643447..faf51e6c853 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -38,7 +38,7 @@ CLIENT_CONNECTED_ATTRIBUTES = [ "ip", "is_11r", "is_guest", - "noted", + "note", "qos_policy_applied", "radio", "radio_proto", @@ -258,13 +258,11 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): """Return the client state attributes.""" raw = self.client.raw + attributes_to_check = CLIENT_STATIC_ATTRIBUTES if self.is_connected: - attributes = { - k: raw[k] for k in CLIENT_CONNECTED_ALL_ATTRIBUTES if k in raw - } - else: - attributes = {k: raw[k] for k in CLIENT_STATIC_ATTRIBUTES if k in raw} + attributes_to_check = CLIENT_CONNECTED_ALL_ATTRIBUTES + attributes = {k: raw[k] for k in attributes_to_check if k in raw} attributes["is_wired"] = self.is_wired return attributes From e037d3a16f868693745003ef44ea5e1383039083 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Tue, 27 Jul 2021 12:58:33 +0200 Subject: [PATCH 629/818] Update spider integration to support HEM (#53397) Co-authored-by: Franck Nijhof --- homeassistant/components/spider/const.py | 2 +- homeassistant/components/spider/sensor.py | 117 ++++++++++++++++++++++ homeassistant/components/spider/switch.py | 12 +-- 3 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/spider/sensor.py diff --git a/homeassistant/components/spider/const.py b/homeassistant/components/spider/const.py index 420767fd221..b8621262ed5 100644 --- a/homeassistant/components/spider/const.py +++ b/homeassistant/components/spider/const.py @@ -3,4 +3,4 @@ DOMAIN = "spider" DEFAULT_SCAN_INTERVAL = 300 -PLATFORMS = ["climate", "switch"] +PLATFORMS = ["climate", "switch", "sensor"] diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py new file mode 100644 index 00000000000..998a9ff8eee --- /dev/null +++ b/homeassistant/components/spider/sensor.py @@ -0,0 +1,117 @@ +"""Support for Spider Powerplugs (energy & power).""" +from datetime import datetime + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.util import dt as dt_util + +from .const import DOMAIN + + +async def async_setup_entry(hass, config, async_add_entities): + """Initialize a Spider Power Plug.""" + api = hass.data[DOMAIN][config.entry_id] + entities = [] + + for entity in await hass.async_add_executor_job(api.get_power_plugs): + entities.append(SpiderPowerPlugEnergy(api, entity)) + entities.append(SpiderPowerPlugPower(api, entity)) + + async_add_entities(entities) + + +class SpiderPowerPlugEnergy(SensorEntity): + """Representation of a Spider Power Plug (energy).""" + + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_state_class = STATE_CLASS_MEASUREMENT + + def __init__(self, api, power_plug) -> None: + """Initialize the Spider Power Plug.""" + self.api = api + self.power_plug = power_plug + + @property + def device_info(self) -> DeviceInfo: + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.power_plug.id)}, + "name": self.power_plug.name, + "manufacturer": self.power_plug.manufacturer, + "model": self.power_plug.model, + } + + @property + def unique_id(self) -> str: + """Return the ID of this sensor.""" + return f"{self.power_plug.id}_total_energy_today" + + @property + def name(self) -> str: + """Return the name of the sensor if any.""" + return f"{self.power_plug.name} Total Energy Today" + + @property + def state(self) -> float: + """Return todays energy usage in Kwh.""" + return round(self.power_plug.today_energy_consumption / 1000, 2) + + @property + def last_reset(self) -> datetime: + """Return the time when last reset; Every midnight.""" + return dt_util.as_utc( + dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + ) + + def update(self) -> None: + """Get the latest data.""" + self.power_plug = self.api.get_power_plug(self.power_plug.id) + + +class SpiderPowerPlugPower(SensorEntity): + """Representation of a Spider Power Plug (power).""" + + _attr_device_class = DEVICE_CLASS_POWER + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = POWER_WATT + + def __init__(self, api, power_plug) -> None: + """Initialize the Spider Power Plug.""" + self.api = api + self.power_plug = power_plug + + @property + def device_info(self) -> DeviceInfo: + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.power_plug.id)}, + "name": self.power_plug.name, + "manufacturer": self.power_plug.manufacturer, + "model": self.power_plug.model, + } + + @property + def unique_id(self) -> str: + """Return the ID of this sensor.""" + return f"{self.power_plug.id}_power_consumption" + + @property + def name(self) -> str: + """Return the name of the sensor if any.""" + return f"{self.power_plug.name} Power Consumption" + + @property + def state(self) -> float: + """Return the current power usage in W.""" + return round(self.power_plug.current_energy_consumption) + + def update(self) -> None: + """Get the latest data.""" + self.power_plug = self.api.get_power_plug(self.power_plug.id) diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index c9a99f3c205..ceb814b234a 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -43,16 +43,6 @@ class SpiderPowerPlug(SwitchEntity): """Return the name of the switch if any.""" return self.power_plug.name - @property - def current_power_w(self): - """Return the current power usage in W.""" - return round(self.power_plug.current_energy_consumption) - - @property - def today_energy_kwh(self): - """Return the current power usage in Kwh.""" - return round(self.power_plug.today_energy_consumption / 1000, 2) - @property def is_on(self): """Return true if switch is on. Standby is on.""" @@ -73,4 +63,4 @@ class SpiderPowerPlug(SwitchEntity): def update(self): """Get the latest data.""" - self.power_plug = self.api.get_power_plug(self.unique_id) + self.power_plug = self.api.get_power_plug(self.power_plug.id) From b51c6668173ae7fb38707ba63a717905db253d0c Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 14:16:01 +0200 Subject: [PATCH 630/818] Replace ServiceCallType with ServiceCall in lcn services (#53547) --- homeassistant/components/lcn/services.py | 33 ++++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 47efc3ea060..8c305d68403 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -11,9 +11,8 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, TIME_SECONDS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ServiceCallType from .const import ( CONF_KEYS, @@ -59,7 +58,7 @@ class LcnServiceCall: """Initialize service call.""" self.hass = hass - def get_device_connection(self, service: ServiceCallType) -> DeviceConnectionType: + def get_device_connection(self, service: ServiceCall) -> DeviceConnectionType: """Get address connection object.""" address, host_name = service.data[CONF_ADDRESS] @@ -73,7 +72,7 @@ class LcnServiceCall: return device_connection raise ValueError("Invalid host name.") - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" raise NotImplementedError @@ -93,7 +92,7 @@ class OutputAbs(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] brightness = service.data[CONF_BRIGHTNESS] @@ -117,7 +116,7 @@ class OutputRel(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] brightness = service.data[CONF_BRIGHTNESS] @@ -138,7 +137,7 @@ class OutputToggle(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] transition = pypck.lcn_defs.time_to_ramp_value( @@ -154,7 +153,7 @@ class Relays(LcnServiceCall): schema = LcnServiceCall.schema.extend({vol.Required(CONF_STATE): is_states_string}) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" states = [ pypck.lcn_defs.RelayStateModifier[state] @@ -175,7 +174,7 @@ class Led(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" led = pypck.lcn_defs.LedPort[service.data[CONF_LED]] led_state = pypck.lcn_defs.LedStatus[service.data[CONF_STATE]] @@ -203,7 +202,7 @@ class VarAbs(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] value = service.data[CONF_VALUE] @@ -220,7 +219,7 @@ class VarReset(LcnServiceCall): {vol.Required(CONF_VARIABLE): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS))} ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] @@ -246,7 +245,7 @@ class VarRel(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] value = service.data[CONF_VALUE] @@ -267,7 +266,7 @@ class LockRegulator(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" setpoint = pypck.lcn_defs.Var[service.data[CONF_SETPOINT]] state = service.data[CONF_STATE] @@ -295,7 +294,7 @@ class SendKeys(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" device_connection = self.get_device_connection(service) @@ -338,7 +337,7 @@ class LockKeys(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" device_connection = self.get_device_connection(service) @@ -375,7 +374,7 @@ class DynText(LcnServiceCall): } ) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" row_id = service.data[CONF_ROW] - 1 text = service.data[CONF_TEXT] @@ -389,7 +388,7 @@ class Pck(LcnServiceCall): schema = LcnServiceCall.schema.extend({vol.Required(CONF_PCK): str}) - async def async_call_service(self, service: ServiceCallType) -> None: + async def async_call_service(self, service: ServiceCall) -> None: """Execute service call.""" pck = service.data[CONF_PCK] device_connection = self.get_device_connection(service) From 27e69037d47ec1d90dd7e4c9f9b5d6db04f66c79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 27 Jul 2021 14:44:58 +0200 Subject: [PATCH 631/818] Use entity descriptions classes in DSMR (#53549) --- homeassistant/components/dsmr/const.py | 138 ++++++++++++------------ homeassistant/components/dsmr/models.py | 14 +-- homeassistant/components/dsmr/sensor.py | 41 ++++--- 3 files changed, 92 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index b26caa5c865..a5e51816183 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ) from homeassistant.util import dt -from .models import DSMRSensor +from .models import DSMRSensorEntityDescription DOMAIN = "dsmr" @@ -41,217 +41,217 @@ DATA_TASK = "task" DEVICE_NAME_ENERGY = "Energy Meter" DEVICE_NAME_GAS = "Gas Meter" -SENSORS: list[DSMRSensor] = [ - DSMRSensor( +SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( + DSMRSensorEntityDescription( + key=obis_references.CURRENT_ELECTRICITY_USAGE, name="Power Consumption", - obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, device_class=DEVICE_CLASS_POWER, force_update=True, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.CURRENT_ELECTRICITY_DELIVERY, name="Power Production", - obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=DEVICE_CLASS_POWER, force_update=True, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_ACTIVE_TARIFF, name="Power Tariff", - obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, icon="mdi:flash", ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_USED_TARIFF_1, name="Energy Consumption (tarif 1)", - obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, device_class=DEVICE_CLASS_ENERGY, force_update=True, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_USED_TARIFF_2, name="Energy Consumption (tarif 2)", - obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, force_update=True, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, name="Energy Production (tarif 1)", - obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, force_update=True, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, name="Energy Production (tarif 2)", - obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, force_update=True, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, name="Power Consumption Phase L1", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, name="Power Consumption Phase L2", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, name="Power Consumption Phase L3", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, name="Power Production Phase L1", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, name="Power Production Phase L2", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, name="Power Production Phase L3", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.SHORT_POWER_FAILURE_COUNT, name="Short Power Failure Count", - obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, entity_registry_enabled_default=False, icon="mdi:flash-off", ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.LONG_POWER_FAILURE_COUNT, name="Long Power Failure Count", - obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, entity_registry_enabled_default=False, icon="mdi:flash-off", ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L1_COUNT, name="Voltage Sags Phase L1", - obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, entity_registry_enabled_default=False, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L2_COUNT, name="Voltage Sags Phase L2", - obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, entity_registry_enabled_default=False, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L3_COUNT, name="Voltage Sags Phase L3", - obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, entity_registry_enabled_default=False, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L1_COUNT, name="Voltage Swells Phase L1", - obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, entity_registry_enabled_default=False, icon="mdi:pulse", ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L2_COUNT, name="Voltage Swells Phase L2", - obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, entity_registry_enabled_default=False, icon="mdi:pulse", ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L3_COUNT, name="Voltage Swells Phase L3", - obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, entity_registry_enabled_default=False, icon="mdi:pulse", ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L1, name="Voltage Phase L1", - obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, device_class=DEVICE_CLASS_VOLTAGE, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L2, name="Voltage Phase L2", - obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, device_class=DEVICE_CLASS_VOLTAGE, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L3, name="Voltage Phase L3", - obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, device_class=DEVICE_CLASS_VOLTAGE, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L1, name="Current Phase L1", - obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, device_class=DEVICE_CLASS_CURRENT, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L2, name="Current Phase L2", - obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, device_class=DEVICE_CLASS_CURRENT, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L3, name="Current Phase L3", - obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, device_class=DEVICE_CLASS_CURRENT, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, name="Energy Consumption (total)", - obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, dsmr_versions={"5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, name="Energy Production (total)", - obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, dsmr_versions={"5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_IMPORTED_TOTAL, name="Energy Consumption (total)", - obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, dsmr_versions={"2.2", "4", "5", "5B"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.HOURLY_GAS_METER_READING, name="Gas Consumption", - obis_reference=obis_references.HOURLY_GAS_METER_READING, dsmr_versions={"4", "5", "5L"}, is_gas=True, force_update=True, @@ -259,9 +259,9 @@ SENSORS: list[DSMRSensor] = [ last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_HOURLY_GAS_METER_READING, name="Gas Consumption", - obis_reference=obis_references.BELGIUM_HOURLY_GAS_METER_READING, dsmr_versions={"5B"}, is_gas=True, force_update=True, @@ -269,9 +269,9 @@ SENSORS: list[DSMRSensor] = [ last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), - DSMRSensor( + DSMRSensorEntityDescription( + key=obis_references.GAS_METER_READING, name="Gas Consumption", - obis_reference=obis_references.GAS_METER_READING, dsmr_versions={"2.2"}, is_gas=True, force_update=True, @@ -279,4 +279,4 @@ SENSORS: list[DSMRSensor] = [ last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), -] +) diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py index b54a5af80d5..e7b47d8b74d 100644 --- a/homeassistant/components/dsmr/models.py +++ b/homeassistant/components/dsmr/models.py @@ -2,21 +2,13 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime + +from homeassistant.components.sensor import SensorEntityDescription @dataclass -class DSMRSensor: +class DSMRSensorEntityDescription(SensorEntityDescription): """Represents an DSMR Sensor.""" - name: str - obis_reference: str - - device_class: str | None = None dsmr_versions: set[str] | None = None - entity_registry_enabled_default: bool = True - force_update: bool = False - icon: str | None = None is_gas: bool = False - last_reset: datetime | None = None - state_class: str | None = None diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index cfdcbd95cf4..d5674b34520 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -42,7 +42,7 @@ from .const import ( LOGGER, SENSORS, ) -from .models import DSMRSensor +from .models import DSMRSensorEntityDescription PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -83,10 +83,13 @@ async def async_setup_entry( """Set up the DSMR sensor.""" dsmr_version = entry.data[CONF_DSMR_VERSION] entities = [ - DSMREntity(sensor, entry) - for sensor in SENSORS - if (sensor.dsmr_versions is None or dsmr_version in sensor.dsmr_versions) - and (not sensor.is_gas or CONF_SERIAL_ID_GAS in entry.data) + DSMREntity(description, entry) + for description in SENSORS + if ( + description.dsmr_versions is None + or dsmr_version in description.dsmr_versions + ) + and (not description.is_gas or CONF_SERIAL_ID_GAS in entry.data) ] async_add_entities(entities) @@ -184,50 +187,46 @@ async def async_setup_entry( class DSMREntity(SensorEntity): """Entity reading values from DSMR telegram.""" + entity_description: DSMRSensorEntityDescription _attr_should_poll = False - def __init__(self, sensor: DSMRSensor, entry: ConfigEntry) -> None: + def __init__( + self, entity_description: DSMRSensorEntityDescription, entry: ConfigEntry + ) -> None: """Initialize entity.""" - self._sensor = sensor + self.entity_description = entity_description self._entry = entry self.telegram: dict[str, DSMRObject] = {} device_serial = entry.data[CONF_SERIAL_ID] device_name = DEVICE_NAME_ENERGY - if sensor.is_gas: + if entity_description.is_gas: device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS - self._attr_device_class = sensor.device_class self._attr_device_info = { "identifiers": {(DOMAIN, device_serial)}, "name": device_name, } - self._attr_entity_registry_enabled_default = ( - sensor.entity_registry_enabled_default + self._attr_unique_id = f"{device_serial}_{entity_description.name}".replace( + " ", "_" ) - self._attr_force_update = sensor.force_update - self._attr_icon = sensor.icon - self._attr_last_reset = sensor.last_reset - self._attr_name = sensor.name - self._attr_state_class = sensor.state_class - self._attr_unique_id = f"{device_serial}_{sensor.name}".replace(" ", "_") @callback def update_data(self, telegram: dict[str, DSMRObject]) -> None: """Update data.""" self.telegram = telegram - if self.hass and self._sensor.obis_reference in self.telegram: + if self.hass and self.entity_description.key in self.telegram: self.async_write_ha_state() def get_dsmr_object_attr(self, attribute: str) -> str | None: """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis - if self._sensor.obis_reference not in self.telegram: + if self.entity_description.key not in self.telegram: return None # Get the attribute value if the object has it - dsmr_object = self.telegram[self._sensor.obis_reference] + dsmr_object = self.telegram[self.entity_description.key] attr: str | None = getattr(dsmr_object, attribute) return attr @@ -238,7 +237,7 @@ class DSMREntity(SensorEntity): if value is None: return None - if self._sensor.obis_reference == obis_ref.ELECTRICITY_ACTIVE_TARIFF: + if self.entity_description.key == obis_ref.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): From e3df4f8795cf540e15b4a97a15a965683f93bd97 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Jul 2021 14:46:51 +0200 Subject: [PATCH 632/818] Upgrade Rituals Perfume Genie to quality level "silver" (#53550) --- homeassistant/components/rituals_perfume_genie/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 9ca7556133d..2daa6e43873 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", "requirements": ["pyrituals==0.0.6"], "codeowners": ["@milanmeu"], + "quality_scale": "silver", "iot_class": "cloud_polling" } From 268ade6b76dd98efd294258b4a582aae3cf82b88 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 16:22:01 +0200 Subject: [PATCH 633/818] Use EntityDescription - metoffice (#53555) --- homeassistant/components/metoffice/sensor.py | 146 +++++++++---------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 1120e75c50a..749282b1a21 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,9 +1,7 @@ """Support for UK Met Office weather service.""" from __future__ import annotations -from typing import NamedTuple - -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_HUMIDITY, @@ -39,102 +37,104 @@ ATTR_SITE_ID = "site_id" ATTR_SITE_NAME = "site_name" -class MetOfficeSensorMetadata(NamedTuple): - """Sensor metadata for an individual NWS sensor.""" - - title: str - device_class: str | None - unit_of_measurement: str | None - icon: str | None - enabled_by_default: bool - - -SENSOR_TYPES = { - "name": MetOfficeSensorMetadata( - "Station Name", +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="name", + name="Station Name", device_class=None, unit_of_measurement=None, icon="mdi:label-outline", - enabled_by_default=False, + entity_registry_enabled_default=False, ), - "weather": MetOfficeSensorMetadata( - "Weather", + SensorEntityDescription( + key="weather", + name="Weather", device_class=None, unit_of_measurement=None, icon="mdi:weather-sunny", # but will adapt to current conditions - enabled_by_default=True, + entity_registry_enabled_default=True, ), - "temperature": MetOfficeSensorMetadata( - "Temperature", + SensorEntityDescription( + key="temperature", + name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, - enabled_by_default=True, + entity_registry_enabled_default=True, ), - "feels_like_temperature": MetOfficeSensorMetadata( - "Feels Like Temperature", + SensorEntityDescription( + key="feels_like_temperature", + name="Feels Like Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, - enabled_by_default=False, + entity_registry_enabled_default=False, ), - "wind_speed": MetOfficeSensorMetadata( - "Wind Speed", + SensorEntityDescription( + key="wind_speed", + name="Wind Speed", device_class=None, unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", - enabled_by_default=True, + entity_registry_enabled_default=True, ), - "wind_direction": MetOfficeSensorMetadata( - "Wind Direction", + SensorEntityDescription( + key="wind_direction", + name="Wind Direction", device_class=None, unit_of_measurement=None, icon="mdi:compass-outline", - enabled_by_default=False, + entity_registry_enabled_default=False, ), - "wind_gust": MetOfficeSensorMetadata( - "Wind Gust", + SensorEntityDescription( + key="wind_gust", + name="Wind Gust", device_class=None, unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", - enabled_by_default=False, + entity_registry_enabled_default=False, ), - "visibility": MetOfficeSensorMetadata( - "Visibility", + SensorEntityDescription( + key="visibility", + name="Visibility", device_class=None, unit_of_measurement=None, icon="mdi:eye", - enabled_by_default=False, + entity_registry_enabled_default=False, ), - "visibility_distance": MetOfficeSensorMetadata( - "Visibility Distance", + SensorEntityDescription( + key="visibility_distance", + name="Visibility Distance", device_class=None, unit_of_measurement=LENGTH_KILOMETERS, icon="mdi:eye", - enabled_by_default=False, + entity_registry_enabled_default=False, ), - "uv": MetOfficeSensorMetadata( - "UV Index", + SensorEntityDescription( + key="uv", + name="UV Index", device_class=None, unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", - enabled_by_default=True, + entity_registry_enabled_default=True, ), - "precipitation": MetOfficeSensorMetadata( - "Probability of Precipitation", + SensorEntityDescription( + key="precipitation", + name="Probability of Precipitation", device_class=None, unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", - enabled_by_default=True, + entity_registry_enabled_default=True, ), - "humidity": MetOfficeSensorMetadata( - "Humidity", + SensorEntityDescription( + key="humidity", + name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, unit_of_measurement=PERCENTAGE, icon=None, - enabled_by_default=False, + entity_registry_enabled_default=False, ), -} +) async def async_setup_entry( @@ -149,20 +149,18 @@ async def async_setup_entry( hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, True, - sensor_type, - metadata, + description, ) - for sensor_type, metadata in SENSOR_TYPES.items() + for description in SENSOR_TYPES ] + [ MetOfficeCurrentSensor( hass_data[METOFFICE_DAILY_COORDINATOR], hass_data, False, - sensor_type, - metadata, + description, ) - for sensor_type, metadata in SENSOR_TYPES.items() + for description in SENSOR_TYPES ], False, ) @@ -176,21 +174,17 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): coordinator, hass_data, use_3hourly, - sensor_type, - metadata: MetOfficeSensorMetadata, + description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__(coordinator) - self._type = sensor_type - self._metadata = metadata + self.entity_description = description mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL - self._attr_name = f"{hass_data[METOFFICE_NAME]} {metadata.title} {mode_label}" - self._attr_unique_id = f"{metadata.title}_{hass_data[METOFFICE_COORDINATES]}" + self._attr_name = f"{hass_data[METOFFICE_NAME]} {description.name} {mode_label}" + self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" - self._attr_device_class = metadata.device_class - self._attr_unit_of_measurement = metadata.unit_of_measurement self.use_3hourly = use_3hourly @@ -199,27 +193,29 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): """Return the state of the sensor.""" value = None - if self._type == "visibility_distance" and hasattr( + if self.entity_description.key == "visibility_distance" and hasattr( self.coordinator.data.now, "visibility" ): value = VISIBILITY_DISTANCE_CLASSES.get( self.coordinator.data.now.visibility.value ) - if self._type == "visibility" and hasattr( + if self.entity_description.key == "visibility" and hasattr( self.coordinator.data.now, "visibility" ): value = VISIBILITY_CLASSES.get(self.coordinator.data.now.visibility.value) - elif self._type == "weather" and hasattr(self.coordinator.data.now, self._type): + elif self.entity_description.key == "weather" and hasattr( + self.coordinator.data.now, self.entity_description.key + ): value = [ k for k, v in CONDITION_CLASSES.items() if self.coordinator.data.now.weather.value in v ][0] - elif hasattr(self.coordinator.data.now, self._type): - value = getattr(self.coordinator.data.now, self._type) + elif hasattr(self.coordinator.data.now, self.entity_description.key): + value = getattr(self.coordinator.data.now, self.entity_description.key) if hasattr(value, "value"): value = value.value @@ -229,8 +225,8 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): @property def icon(self): """Return the icon for the entity card.""" - value = self._metadata.icon - if self._type == "weather": + value = self.entity_description.icon + if self.entity_description.key == "weather": value = self.state if value is None: value = "sunny" @@ -246,7 +242,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_LAST_UPDATE: self.coordinator.data.now.date, - ATTR_SENSOR_ID: self._type, + ATTR_SENSOR_ID: self.entity_description.key, ATTR_SITE_ID: self.coordinator.data.site.id, ATTR_SITE_NAME: self.coordinator.data.site.name, } @@ -254,4 +250,6 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._metadata.enabled_by_default and self.use_3hourly + return ( + self.entity_description.entity_registry_enabled_default and self.use_3hourly + ) From 214920f48615bfbc7ca0cb6cc7de35a95eefc2dd Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 27 Jul 2021 09:50:46 -0500 Subject: [PATCH 634/818] Ignore Sonos Boost devices during discovery v2 (#53358) --- homeassistant/components/sonos/__init__.py | 44 +++++++++++--------- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sonos/test_media_player.py | 13 ++++++ 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index b7f17752097..f0219ea8cf0 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -12,7 +12,7 @@ from urllib.parse import urlparse from soco import events_asyncio import soco.config as soco_config from soco.core import SoCo -from soco.exceptions import SoCoException +from soco.exceptions import NotSupportedException, SoCoException import voluptuous as vol from homeassistant import config_entries @@ -87,6 +87,7 @@ class SonosData: self.alarms: dict[str, SonosAlarms] = {} self.topology_condition = asyncio.Condition() self.hosts_heartbeat = None + self.discovery_ignored: set[str] = set() self.discovery_known: set[str] = set() self.boot_counts: dict[str, int] = {} @@ -137,21 +138,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None: - """Create a soco instance and return if successful.""" - try: - soco = SoCo(ip_address) - # Ensure that the player is available and UID is cached - _ = soco.uid - _ = soco.volume - return soco - except (OSError, SoCoException) as ex: - _LOGGER.warning( - "Failed to connect to %s player '%s': %s", source.value, ip_address, ex - ) - return None - - class SonosDiscoveryManager: """Manage sonos discovery.""" @@ -165,6 +151,26 @@ class SonosDiscoveryManager: self.hosts = hosts self.discovery_lock = asyncio.Lock() + def _create_soco(self, ip_address: str, source: SoCoCreationSource) -> SoCo | None: + """Create a soco instance and return if successful.""" + if ip_address in self.data.discovery_ignored: + return None + + try: + soco = SoCo(ip_address) + # Ensure that the player is available and UID is cached + uid = soco.uid + _ = soco.volume + return soco + except NotSupportedException as exc: + _LOGGER.debug("Device %s is not supported, ignoring: %s", uid, exc) + self.data.discovery_ignored.add(ip_address) + except (OSError, SoCoException) as ex: + _LOGGER.warning( + "Failed to connect to %s player '%s': %s", source.value, ip_address, ex + ) + return None + async def _async_stop_event_listener(self, event: Event) -> None: await asyncio.gather( *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()), @@ -213,7 +219,7 @@ class SonosDiscoveryManager: if known_uid: dispatcher_send(self.hass, f"{SONOS_SEEN}-{known_uid}") else: - soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED) + soco = self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED) if soco and soco.is_visible: self._discovered_player(soco) @@ -222,7 +228,7 @@ class SonosDiscoveryManager: ) def _discovered_ip(self, ip_address): - soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED) + soco = self._create_soco(ip_address, SoCoCreationSource.DISCOVERED) if soco and soco.is_visible: self._discovered_player(soco) @@ -238,7 +244,7 @@ class SonosDiscoveryManager: if boot_seqnum and boot_seqnum > self.data.boot_counts[uid]: self.data.boot_counts[uid] = boot_seqnum if soco := await self.hass.async_add_executor_job( - _create_soco, discovered_ip, SoCoCreationSource.REBOOTED + self._create_soco, discovered_ip, SoCoCreationSource.REBOOTED ): async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}", soco) else: diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e571693c659..4ce5623ac38 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.23.1"], + "requirements": ["soco==0.23.2"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index dfbd6090ffb..d3b0759ea11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2148,7 +2148,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.23.1 +soco==0.23.2 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63caf5e42d3..c135613db5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1177,7 +1177,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.23.1 +soco==0.23.2 # homeassistant.components.solaredge solaredge==0.0.2 diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index cba6967463a..0e4af2071b2 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,8 @@ """Tests for the Sonos Media Player platform.""" +from unittest.mock import PropertyMock + import pytest +from soco.exceptions import NotSupportedException from homeassistant.components.sonos import DATA_SONOS, DOMAIN, media_player from homeassistant.const import STATE_IDLE @@ -40,6 +43,16 @@ async def test_async_setup_entry_discover(hass, config_entry, discover): assert media_player.state == STATE_IDLE +async def test_discovery_ignore_unsupported_device(hass, config_entry, soco, caplog): + """Test discovery setup.""" + message = f"GetVolume not supported on {soco.ip_address}" + type(soco).volume = PropertyMock(side_effect=NotSupportedException(message)) + await setup_platform(hass, config_entry, {}) + + assert message in caplog.text + assert not hass.data[DATA_SONOS].discovered + + async def test_services(hass, config_entry, config, hass_read_only_user): """Test join/unjoin requires control access.""" await setup_platform(hass, config_entry, config) From 0552b9c783156d7e6e2aafb9f3cafc569107d1c5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 17:24:18 +0200 Subject: [PATCH 635/818] Use EntityDescription - glances (#53559) --- homeassistant/components/glances/const.py | 85 +++++++++++++-------- homeassistant/components/glances/sensor.py | 86 ++++++++++------------ 2 files changed, 92 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 56d55931cdb..b74662db22b 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,9 +1,10 @@ """Constants for Glances component.""" from __future__ import annotations +from dataclasses import dataclass import sys -from typing import NamedTuple +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import ( DATA_GIBIBYTES, DATA_MEBIBYTES, @@ -30,147 +31,167 @@ else: CPU_ICON = "mdi:cpu-32-bit" -class GlancesSensorMetadata(NamedTuple): - """Sensor metadata for an individual Glances sensor.""" +@dataclass +class GlancesSensorEntityDescription(SensorEntityDescription): + """Describe Glances sensor entity.""" - type: str - name_suffix: str - unit_of_measurement: str - icon: str | None = None - device_class: str | None = None + type: str | None = None + name_suffix: str | None = None -SENSOR_TYPES: dict[str, GlancesSensorMetadata] = { - "disk_use_percent": GlancesSensorMetadata( +SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( + GlancesSensorEntityDescription( + key="disk_use_percent", type="fs", name_suffix="used percent", unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", ), - "disk_use": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="disk_use", type="fs", name_suffix="used", unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", ), - "disk_free": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="disk_free", type="fs", name_suffix="free", unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", ), - "memory_use_percent": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="memory_use_percent", type="mem", name_suffix="RAM used percent", unit_of_measurement=PERCENTAGE, icon="mdi:memory", ), - "memory_use": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="memory_use", type="mem", name_suffix="RAM used", unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", ), - "memory_free": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="memory_free", type="mem", name_suffix="RAM free", unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", ), - "swap_use_percent": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="swap_use_percent", type="memswap", name_suffix="Swap used percent", unit_of_measurement=PERCENTAGE, icon="mdi:memory", ), - "swap_use": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="swap_use", type="memswap", name_suffix="Swap used", unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", ), - "swap_free": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="swap_free", type="memswap", name_suffix="Swap free", unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", ), - "processor_load": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="processor_load", type="load", name_suffix="CPU load", unit_of_measurement="15 min", icon=CPU_ICON, ), - "process_running": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="process_running", type="processcount", name_suffix="Running", unit_of_measurement="Count", icon=CPU_ICON, ), - "process_total": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="process_total", type="processcount", name_suffix="Total", unit_of_measurement="Count", icon=CPU_ICON, ), - "process_thread": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="process_thread", type="processcount", name_suffix="Thread", unit_of_measurement="Count", icon=CPU_ICON, ), - "process_sleeping": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="process_sleeping", type="processcount", name_suffix="Sleeping", unit_of_measurement="Count", icon=CPU_ICON, ), - "cpu_use_percent": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="cpu_use_percent", type="cpu", name_suffix="CPU used", unit_of_measurement=PERCENTAGE, icon=CPU_ICON, ), - "temperature_core": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="temperature_core", type="sensors", name_suffix="Temperature", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), - "temperature_hdd": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="temperature_hdd", type="sensors", name_suffix="Temperature", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), - "fan_speed": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="fan_speed", type="sensors", name_suffix="Fan speed", unit_of_measurement="RPM", icon="mdi:fan", ), - "battery": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="battery", type="sensors", name_suffix="Charge", unit_of_measurement=PERCENTAGE, icon="mdi:battery", ), - "docker_active": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="docker_active", type="docker", name_suffix="Containers active", unit_of_measurement="", icon="mdi:docker", ), - "docker_cpu_use": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="docker_cpu_use", type="docker", name_suffix="Containers CPU used", unit_of_measurement=PERCENTAGE, icon="mdi:docker", ), - "docker_memory_use": GlancesSensorMetadata( + GlancesSensorEntityDescription( + key="docker_memory_use", type="docker", name_suffix="Containers RAM used", unit_of_measurement=DATA_MEBIBYTES, icon="mdi:docker", ), -} +) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index e33fd121200..fd31ee37faf 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -4,7 +4,7 @@ from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorMetadata +from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorEntityDescription async def async_setup_entry(hass, config_entry, async_add_entities): @@ -14,42 +14,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = config_entry.data[CONF_NAME] dev = [] - for sensor_type, metadata in SENSOR_TYPES.items(): - if metadata.type not in client.api.data: - continue - if metadata.type == "fs": + for description in SENSOR_TYPES: + if description.type == "fs": # fs will provide a list of disks attached - for disk in client.api.data[metadata.type]: + for disk in client.api.data[description.type]: dev.append( GlancesSensor( client, name, disk["mnt_point"], - sensor_type, - metadata, + description, ) ) - elif metadata.type == "sensors": + elif description.type == "sensors": # sensors will provide temp for different devices - for sensor in client.api.data[metadata.type]: - if sensor["type"] == sensor_type: + for sensor in client.api.data[description.type]: + if sensor["type"] == description.key: dev.append( GlancesSensor( client, name, sensor["label"], - sensor_type, - metadata, + description, ) ) - elif client.api.data[metadata.type]: + elif client.api.data[description.type]: dev.append( GlancesSensor( client, name, "", - sensor_type, - metadata, + description, ) ) @@ -59,26 +54,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class GlancesSensor(SensorEntity): """Implementation of a Glances sensor.""" + entity_description: GlancesSensorEntityDescription + def __init__( self, glances_data, name, sensor_name_prefix, - sensor_type, - metadata: GlancesSensorMetadata, + description: GlancesSensorEntityDescription, ): """Initialize the sensor.""" self.glances_data = glances_data self._sensor_name_prefix = sensor_name_prefix - self.type = sensor_type self._state = None - self._metadata = metadata self.unsub_update = None - self._attr_name = f"{name} {sensor_name_prefix} {metadata.name_suffix}" - self._attr_icon = metadata.icon - self._attr_unit_of_measurement = metadata.unit_of_measurement - self._attr_device_class = metadata.device_class + self.entity_description = description + self._attr_name = f"{name} {sensor_name_prefix} {description.name_suffix}" @property def unique_id(self): @@ -122,12 +114,12 @@ class GlancesSensor(SensorEntity): if value is None: return - if self._metadata.type == "fs": + if self.entity_description.type == "fs": for var in value["fs"]: if var["mnt_point"] == self._sensor_name_prefix: disk = var break - if self.type == "disk_free": + if self.entity_description.key == "disk_free": try: self._state = round(disk["free"] / 1024 ** 3, 1) except KeyError: @@ -135,67 +127,67 @@ class GlancesSensor(SensorEntity): (disk["size"] - disk["used"]) / 1024 ** 3, 1, ) - elif self.type == "disk_use": + elif self.entity_description.key == "disk_use": self._state = round(disk["used"] / 1024 ** 3, 1) - elif self.type == "disk_use_percent": + elif self.entity_description.key == "disk_use_percent": self._state = disk["percent"] - elif self.type == "battery": + elif self.entity_description.key == "battery": for sensor in value["sensors"]: if ( sensor["type"] == "battery" and sensor["label"] == self._sensor_name_prefix ): self._state = sensor["value"] - elif self.type == "fan_speed": + elif self.entity_description.key == "fan_speed": for sensor in value["sensors"]: if ( sensor["type"] == "fan_speed" and sensor["label"] == self._sensor_name_prefix ): self._state = sensor["value"] - elif self.type == "temperature_core": + elif self.entity_description.key == "temperature_core": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_core" and sensor["label"] == self._sensor_name_prefix ): self._state = sensor["value"] - elif self.type == "temperature_hdd": + elif self.entity_description.key == "temperature_hdd": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_hdd" and sensor["label"] == self._sensor_name_prefix ): self._state = sensor["value"] - elif self.type == "memory_use_percent": + elif self.entity_description.key == "memory_use_percent": self._state = value["mem"]["percent"] - elif self.type == "memory_use": + elif self.entity_description.key == "memory_use": self._state = round(value["mem"]["used"] / 1024 ** 2, 1) - elif self.type == "memory_free": + elif self.entity_description.key == "memory_free": self._state = round(value["mem"]["free"] / 1024 ** 2, 1) - elif self.type == "swap_use_percent": + elif self.entity_description.key == "swap_use_percent": self._state = value["memswap"]["percent"] - elif self.type == "swap_use": + elif self.entity_description.key == "swap_use": self._state = round(value["memswap"]["used"] / 1024 ** 3, 1) - elif self.type == "swap_free": + elif self.entity_description.key == "swap_free": self._state = round(value["memswap"]["free"] / 1024 ** 3, 1) - elif self.type == "processor_load": + elif self.entity_description.key == "processor_load": # Windows systems don't provide load details try: self._state = value["load"]["min15"] except KeyError: self._state = value["cpu"]["total"] - elif self.type == "process_running": + elif self.entity_description.key == "process_running": self._state = value["processcount"]["running"] - elif self.type == "process_total": + elif self.entity_description.key == "process_total": self._state = value["processcount"]["total"] - elif self.type == "process_thread": + elif self.entity_description.key == "process_thread": self._state = value["processcount"]["thread"] - elif self.type == "process_sleeping": + elif self.entity_description.key == "process_sleeping": self._state = value["processcount"]["sleeping"] - elif self.type == "cpu_use_percent": + elif self.entity_description.key == "cpu_use_percent": self._state = value["quicklook"]["cpu"] - elif self.type == "docker_active": + elif self.entity_description.key == "docker_active": count = 0 try: for container in value["docker"]["containers"]: @@ -204,7 +196,7 @@ class GlancesSensor(SensorEntity): self._state = count except KeyError: self._state = count - elif self.type == "docker_cpu_use": + elif self.entity_description.key == "docker_cpu_use": cpu_use = 0.0 try: for container in value["docker"]["containers"]: @@ -213,7 +205,7 @@ class GlancesSensor(SensorEntity): self._state = round(cpu_use, 1) except KeyError: self._state = STATE_UNAVAILABLE - elif self.type == "docker_memory_use": + elif self.entity_description.key == "docker_memory_use": mem_use = 0.0 try: for container in value["docker"]["containers"]: From ea9d312b455fabc76e4dfb73dcbfde79c08ed752 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 27 Jul 2021 17:26:47 +0200 Subject: [PATCH 636/818] Use SensorEntityDescription in Brother integration (#53558) --- homeassistant/components/brother/const.py | 159 ++++++++++----------- homeassistant/components/brother/model.py | 15 -- homeassistant/components/brother/sensor.py | 45 +++--- 3 files changed, 101 insertions(+), 118 deletions(-) delete mode 100644 homeassistant/components/brother/model.py diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 727a67d9093..95ffcf063f2 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -3,11 +3,12 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE -from .model import BrotherSensorMetadata - ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" ATTR_BLACK_DRUM_REMAINING_LIFE: Final = "black_drum_remaining_life" @@ -76,172 +77,170 @@ ATTRS_MAP: Final[dict[str, tuple[str, str]]] = { ), } -SENSOR_TYPES: Final[dict[str, BrotherSensorMetadata]] = { - ATTR_STATUS: BrotherSensorMetadata( +SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + key=ATTR_STATUS, icon="mdi:printer", - label=ATTR_STATUS.title(), - unit_of_measurement=None, - enabled=True, + name=ATTR_STATUS.title(), ), - ATTR_PAGE_COUNTER: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", - label=ATTR_PAGE_COUNTER.replace("_", " ").title(), + name=ATTR_PAGE_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_BW_COUNTER: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", - label=ATTR_BW_COUNTER.replace("_", " ").title(), + name=ATTR_BW_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_COLOR_COUNTER: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", - label=ATTR_COLOR_COUNTER.replace("_", " ").title(), + name=ATTR_COLOR_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_DUPLEX_COUNTER: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", - label=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - label=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_BLACK_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - label=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_CYAN_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - label=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_MAGENTA_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - label=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_YELLOW_DRUM_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - label=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_BELT_UNIT_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", - label=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_FUSER_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", - label=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_LASER_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", - label=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_PF_KIT_1_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", - label=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_PF_KIT_MP_REMAINING_LIFE: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", - label=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_BLACK_TONER_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_CYAN_TONER_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_MAGENTA_TONER_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_YELLOW_TONER_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_BLACK_INK_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_CYAN_INK_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_MAGENTA_INK_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_YELLOW_INK_REMAINING: BrotherSensorMetadata( + SensorEntityDescription( + key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", - label=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE, - enabled=True, state_class=STATE_CLASS_MEASUREMENT, ), - ATTR_UPTIME: BrotherSensorMetadata( - icon=None, - label=ATTR_UPTIME.title(), - unit_of_measurement=None, - enabled=False, + SensorEntityDescription( + key=ATTR_UPTIME, + name=ATTR_UPTIME.title(), + entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, ), -} +) diff --git a/homeassistant/components/brother/model.py b/homeassistant/components/brother/model.py deleted file mode 100644 index a1fcc83aae9..00000000000 --- a/homeassistant/components/brother/model.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Type definitions for Brother integration.""" -from __future__ import annotations - -from typing import NamedTuple - - -class BrotherSensorMetadata(NamedTuple): - """Metadata for an individual Brother sensor.""" - - icon: str | None - label: str - unit_of_measurement: str | None - enabled: bool - state_class: str | None = None - device_class: str | None = None diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 90a73f1bd9b..0ff5c14d9cc 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,13 +1,14 @@ """Support for the Brother service.""" from __future__ import annotations -from typing import Any +from typing import Any, cast -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BrotherDataUpdateCoordinator @@ -21,7 +22,6 @@ from .const import ( DOMAIN, SENSOR_TYPES, ) -from .model import BrotherSensorMetadata async def async_setup_entry( @@ -40,11 +40,9 @@ async def async_setup_entry( "sw_version": getattr(coordinator.data, "firmware", None), } - for sensor, metadata in SENSOR_TYPES.items(): - if sensor in coordinator.data: - sensors.append( - BrotherPrinterSensor(coordinator, sensor, metadata, device_info) - ) + for description in SENSOR_TYPES: + if description.key in coordinator.data: + sensors.append(BrotherPrinterSensor(coordinator, description, device_info)) async_add_entities(sensors, False) @@ -54,34 +52,35 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: BrotherDataUpdateCoordinator, - kind: str, - metadata: BrotherSensorMetadata, + description: SensorEntityDescription, device_info: DeviceInfo, ) -> None: """Initialize.""" super().__init__(coordinator) self._attrs: dict[str, Any] = {} - self._attr_device_class = metadata.device_class self._attr_device_info = device_info - self._attr_entity_registry_enabled_default = metadata.enabled - self._attr_icon = metadata.icon - self._attr_name = f"{coordinator.data.model} {metadata.label}" - self._attr_state_class = metadata.state_class - self._attr_unique_id = f"{coordinator.data.serial.lower()}_{kind}" - self._attr_unit_of_measurement = metadata.unit_of_measurement - self.kind = kind + self._attr_name = f"{coordinator.data.model} {description.name}" + self._attr_unique_id = f"{coordinator.data.serial.lower()}_{description.key}" + self.entity_description = description @property - def state(self) -> Any: + def state(self) -> StateType: """Return the state.""" - if self.kind == ATTR_UPTIME: - return getattr(self.coordinator.data, self.kind).isoformat() - return getattr(self.coordinator.data, self.kind) + if self.entity_description.key == ATTR_UPTIME: + return cast( + StateType, + getattr(self.coordinator.data, self.entity_description.key).isoformat(), + ) + return cast( + StateType, getattr(self.coordinator.data, self.entity_description.key) + ) @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - remaining_pages, drum_counter = ATTRS_MAP.get(self.kind, (None, None)) + remaining_pages, drum_counter = ATTRS_MAP.get( + self.entity_description.key, (None, None) + ) if remaining_pages and drum_counter: self._attrs[ATTR_REMAINING_PAGES] = getattr( self.coordinator.data, remaining_pages From 9f495fd200e7a4d9154dcd5e9bcecf40bd4145f6 Mon Sep 17 00:00:00 2001 From: gjong Date: Tue, 27 Jul 2021 17:42:15 +0200 Subject: [PATCH 637/818] Add initial version for the YouLess integration (#41942) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/youless/__init__.py | 58 ++++++ .../components/youless/config_flow.py | 50 +++++ homeassistant/components/youless/const.py | 3 + .../components/youless/manifest.json | 9 + homeassistant/components/youless/sensor.py | 197 ++++++++++++++++++ homeassistant/components/youless/strings.json | 15 ++ .../components/youless/translations/en.json | 21 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/youless/__init__.py | 1 + tests/components/youless/test_config_flows.py | 72 +++++++ 14 files changed, 437 insertions(+) create mode 100644 homeassistant/components/youless/__init__.py create mode 100644 homeassistant/components/youless/config_flow.py create mode 100644 homeassistant/components/youless/const.py create mode 100644 homeassistant/components/youless/manifest.json create mode 100644 homeassistant/components/youless/sensor.py create mode 100644 homeassistant/components/youless/strings.json create mode 100644 homeassistant/components/youless/translations/en.json create mode 100644 tests/components/youless/__init__.py create mode 100644 tests/components/youless/test_config_flows.py diff --git a/.coveragerc b/.coveragerc index 9a89bf842c6..22e81dc57c8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1224,6 +1224,9 @@ omit = homeassistant/components/yandex_transport/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py + homeassistant/components/youless/__init__.py + homeassistant/components/youless/const.py + homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* homeassistant/components/zamg/sensor.py homeassistant/components/zamg/weather.py diff --git a/CODEOWNERS b/CODEOWNERS index 9562817b27f..9ce94438a30 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -585,6 +585,7 @@ homeassistant/components/yandex_transport/* @rishatik92 @devbis homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya +homeassistant/components/youless/* @gjong homeassistant/components/zeroconf/* @bdraco homeassistant/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py new file mode 100644 index 00000000000..83c8209f558 --- /dev/null +++ b/homeassistant/components/youless/__init__.py @@ -0,0 +1,58 @@ +"""The youless integration.""" +from datetime import timedelta +import logging +from urllib.error import URLError + +from youless_api import YoulessAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +PLATFORMS = ["sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up youless from a config entry.""" + api = YoulessAPI(entry.data[CONF_HOST]) + + try: + await hass.async_add_executor_job(api.initialize) + except URLError as exception: + raise ConfigEntryNotReady from exception + + async def async_update_data(): + """Fetch data from the API.""" + await hass.async_add_executor_job(api.update) + return api + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="youless_gateway", + update_method=async_update_data, + update_interval=timedelta(seconds=2), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/youless/config_flow.py b/homeassistant/components/youless/config_flow.py new file mode 100644 index 00000000000..2cf79ae64e0 --- /dev/null +++ b/homeassistant/components/youless/config_flow.py @@ -0,0 +1,50 @@ +"""Config flow for youless integration.""" +from __future__ import annotations + +import logging +from typing import Any +from urllib.error import HTTPError, URLError + +import voluptuous as vol +from youless_api import YoulessAPI + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + + +class YoulessConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for youless.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + api = YoulessAPI(user_input[CONF_HOST]) + await self.hass.async_add_executor_job(api.initialize) + except (HTTPError, URLError): + _LOGGER.exception("Cannot connect to host") + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_HOST], + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_DEVICE: api.mac_address, + }, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/youless/const.py b/homeassistant/components/youless/const.py new file mode 100644 index 00000000000..adbfc521363 --- /dev/null +++ b/homeassistant/components/youless/const.py @@ -0,0 +1,3 @@ +"""Constants for the youless integration.""" + +DOMAIN = "youless" diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json new file mode 100644 index 00000000000..d00f0457b85 --- /dev/null +++ b/homeassistant/components/youless/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "youless", + "name": "YouLess", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/youless", + "requirements": ["youless-api==0.10"], + "codeowners": ["@gjong"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py new file mode 100644 index 00000000000..54155034919 --- /dev/null +++ b/homeassistant/components/youless/sensor.py @@ -0,0 +1,197 @@ +"""The sensor entity for the Youless integration.""" +from __future__ import annotations + +from youless_api.youless_sensor import YoulessSensor + +from homeassistant.components.youless import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE, DEVICE_CLASS_POWER +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Initialize the integration.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + device = entry.data[CONF_DEVICE] + if device is None: + device = entry.entry_id + + async_add_entities( + [ + GasSensor(coordinator, device), + PowerMeterSensor(coordinator, device, "low"), + PowerMeterSensor(coordinator, device, "high"), + PowerMeterSensor(coordinator, device, "total"), + CurrentPowerSensor(coordinator, device), + DeliveryMeterSensor(coordinator, device, "low"), + DeliveryMeterSensor(coordinator, device, "high"), + ExtraMeterSensor(coordinator, device, "total"), + ExtraMeterSensor(coordinator, device, "usage"), + ] + ) + + +class YoulessBaseSensor(CoordinatorEntity, Entity): + """The base sensor for Youless.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + device: str, + device_group: str, + friendly_name: str, + sensor_id: str, + ) -> None: + """Create the sensor.""" + super().__init__(coordinator) + self._device = device + self._device_group = device_group + self._sensor_id = sensor_id + + self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}" + self._attr_device_info = { + "identifiers": {(DOMAIN, f"{device}_{device_group}")}, + "name": friendly_name, + "manufacturer": "YouLess", + "model": self.coordinator.data.model, + } + + @property + def get_sensor(self) -> YoulessSensor | None: + """Property to get the underlying sensor object.""" + return None + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement for the sensor.""" + if self.get_sensor is None: + return None + + return self.get_sensor.unit_of_measurement + + @property + def state(self) -> StateType: + """Determine the state value, only if a sensor is initialized.""" + if self.get_sensor is None: + return None + + return self.get_sensor.value + + @property + def available(self) -> bool: + """Return a flag to indicate the sensor not being available.""" + return super().available and self.get_sensor is not None + + +class GasSensor(YoulessBaseSensor): + """The Youless gas sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: + """Instantiate a gas sensor.""" + super().__init__(coordinator, device, "gas", "Gas meter", "gas") + self._attr_name = "Gas usage" + self._attr_icon = "mdi:fire" + + @property + def get_sensor(self) -> YoulessSensor | None: + """Get the sensor for providing the value.""" + return self.coordinator.data.gas_meter + + +class CurrentPowerSensor(YoulessBaseSensor): + """The current power usage sensor.""" + + _attr_device_class = DEVICE_CLASS_POWER + + def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: + """Instantiate the usage meter.""" + super().__init__(coordinator, device, "power", "Power usage", "usage") + self._device = device + self._attr_name = "Power Usage" + + @property + def get_sensor(self) -> YoulessSensor | None: + """Get the sensor for providing the value.""" + return self.coordinator.data.current_power_usage + + +class DeliveryMeterSensor(YoulessBaseSensor): + """The Youless delivery meter value sensor.""" + + _attr_device_class = DEVICE_CLASS_POWER + + def __init__( + self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + ) -> None: + """Instantiate a delivery meter sensor.""" + super().__init__( + coordinator, device, "delivery", "Power delivery", f"delivery_{dev_type}" + ) + self._type = dev_type + self._attr_name = f"Power delivery {dev_type}" + + @property + def get_sensor(self) -> YoulessSensor | None: + """Get the sensor for providing the value.""" + if self.coordinator.data.delivery_meter is None: + return None + + return getattr(self.coordinator.data.delivery_meter, f"_{self._type}", None) + + +class PowerMeterSensor(YoulessBaseSensor): + """The Youless low meter value sensor.""" + + _attr_device_class = DEVICE_CLASS_POWER + + def __init__( + self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + ) -> None: + """Instantiate a power meter sensor.""" + super().__init__( + coordinator, device, "power", "Power usage", f"power_{dev_type}" + ) + self._device = device + self._type = dev_type + self._attr_name = f"Power {dev_type}" + + @property + def get_sensor(self) -> YoulessSensor | None: + """Get the sensor for providing the value.""" + if self.coordinator.data.power_meter is None: + return None + + return getattr(self.coordinator.data.power_meter, f"_{self._type}", None) + + +class ExtraMeterSensor(YoulessBaseSensor): + """The Youless extra meter value sensor (s0).""" + + _attr_device_class = DEVICE_CLASS_POWER + + def __init__( + self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + ) -> None: + """Instantiate an extra meter sensor.""" + super().__init__( + coordinator, device, "extra", "Extra meter", f"extra_{dev_type}" + ) + self._type = dev_type + self._attr_name = f"Extra {dev_type}" + + @property + def get_sensor(self) -> YoulessSensor | None: + """Get the sensor for providing the value.""" + if self.coordinator.data.extra_meter is None: + return None + + return getattr(self.coordinator.data.extra_meter, f"_{self._type}", None) diff --git a/homeassistant/components/youless/strings.json b/homeassistant/components/youless/strings.json new file mode 100644 index 00000000000..3728db7ffe6 --- /dev/null +++ b/homeassistant/components/youless/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/en.json b/homeassistant/components/youless/translations/en.json new file mode 100644 index 00000000000..38923682b10 --- /dev/null +++ b/homeassistant/components/youless/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 270304cb623..445f1cbde66 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -303,6 +303,7 @@ FLOWS = [ "yale_smart_alarm", "yamaha_musiccast", "yeelight", + "youless", "zerproc", "zha", "zwave", diff --git a/requirements_all.txt b/requirements_all.txt index d3b0759ea11..94f6960cc7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2422,6 +2422,9 @@ yeelight==0.6.3 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 +# homeassistant.components.youless +youless-api==0.10 + # homeassistant.components.media_extractor youtube_dl==2021.04.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c135613db5e..26123a6a672 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,6 +1334,9 @@ yalexs==1.1.12 # homeassistant.components.yeelight yeelight==0.6.3 +# homeassistant.components.youless +youless-api==0.10 + # homeassistant.components.onvif zeep[async]==4.0.0 diff --git a/tests/components/youless/__init__.py b/tests/components/youless/__init__.py new file mode 100644 index 00000000000..8711c6721bc --- /dev/null +++ b/tests/components/youless/__init__.py @@ -0,0 +1 @@ +"""Tests for the youless component.""" diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py new file mode 100644 index 00000000000..d7d9a39ec6e --- /dev/null +++ b/tests/components/youless/test_config_flows.py @@ -0,0 +1,72 @@ +"""Test the youless config flow.""" +from unittest.mock import MagicMock, patch +from urllib.error import URLError + +from homeassistant.components.youless import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +def _get_mock_youless_api(initialize=None): + mock_youless = MagicMock() + if isinstance(initialize, Exception): + type(mock_youless).initialize = MagicMock(side_effect=initialize) + else: + type(mock_youless).initialize = MagicMock(return_value=initialize) + + type(mock_youless).mac_address = None + return mock_youless + + +async def test_full_flow(hass: HomeAssistant) -> None: + """Check setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_youless = _get_mock_youless_api( + initialize={"homes": [{"id": 1, "name": "myhome"}]} + ) + with patch( + "homeassistant.components.youless.config_flow.YoulessAPI", + return_value=mock_youless, + ) as mocked_youless: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "localhost"}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "localhost" + assert len(mocked_youless.mock_calls) == 1 + + +async def test_not_found(hass: HomeAssistant) -> None: + """Check setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_youless = _get_mock_youless_api(initialize=URLError("")) + with patch( + "homeassistant.components.youless.config_flow.YoulessAPI", + return_value=mock_youless, + ) as mocked_youless: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "localhost"}, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert len(mocked_youless.mock_calls) == 1 From 72a98550b63aca226894abfab2d8cb864ae39fb6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 17:45:25 +0200 Subject: [PATCH 638/818] Use EntityDescription - epsonworkforce (#53556) --- .../components/epsonworkforce/sensor.py | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 65a5e6342f1..2f483b9fcbf 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -2,57 +2,59 @@ from __future__ import annotations from datetime import timedelta -from typing import NamedTuple from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="black", + name="Ink level Black", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="photoblack", + name="Ink level Photoblack", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="magenta", + name="Ink level Magenta", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="cyan", + name="Ink level Cyan", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="yellow", + name="Ink level Yellow", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="clean", + name="Cleaning level", + icon="mdi:water", + unit_of_measurement=PERCENTAGE, + ), +) +MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] -class MonitoredConditionsMetadata(NamedTuple): - """Metadata for an individual montiored condition.""" - - name: str - icon: str - unit_of_measurement: str - - -MONITORED_CONDITIONS: dict[str, MonitoredConditionsMetadata] = { - "black": MonitoredConditionsMetadata( - "Ink level Black", - icon="mdi:water", - unit_of_measurement=PERCENTAGE, - ), - "photoblack": MonitoredConditionsMetadata( - "Ink level Photoblack", - icon="mdi:water", - unit_of_measurement=PERCENTAGE, - ), - "magenta": MonitoredConditionsMetadata( - "Ink level Magenta", - icon="mdi:water", - unit_of_measurement=PERCENTAGE, - ), - "cyan": MonitoredConditionsMetadata( - "Ink level Cyan", - icon="mdi:water", - unit_of_measurement=PERCENTAGE, - ), - "yellow": MonitoredConditionsMetadata( - "Ink level Yellow", - icon="mdi:water", - unit_of_measurement=PERCENTAGE, - ), - "clean": MonitoredConditionsMetadata( - "Cleaning level", - icon="mdi:water", - unit_of_measurement=PERCENTAGE, - ), -} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -73,8 +75,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): raise PlatformNotReady() sensors = [ - EpsonPrinterCartridge(api, condition) - for condition in config[CONF_MONITORED_CONDITIONS] + EpsonPrinterCartridge(api, description) + for description in SENSOR_TYPES + if description.key in config[CONF_MONITORED_CONDITIONS] ] add_devices(sensors, True) @@ -83,20 +86,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class EpsonPrinterCartridge(SensorEntity): """Representation of a cartridge sensor.""" - def __init__(self, api, cartridgeidx): + def __init__(self, api, description: SensorEntityDescription): """Initialize a cartridge sensor.""" self._api = api - - self._id = cartridgeidx - metadata = MONITORED_CONDITIONS[self._id] - self._attr_name = metadata.name - self._attr_icon = metadata.icon - self._attr_unit_of_measurement = metadata.unit_of_measurement + self.entity_description = description @property def state(self): """Return the state of the device.""" - return self._api.getSensorValue(self._id) + return self._api.getSensorValue(self.entity_description.key) @property def available(self): From f1eb35b1a5b94c3c8ab1057bf0991091fc2f633a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 27 Jul 2021 17:45:47 +0200 Subject: [PATCH 639/818] Use entity descriptions classes in Forecast.Solar (#53553) --- .../components/forecast_solar/const.py | 26 +++++++-------- .../components/forecast_solar/models.py | 13 +++----- .../components/forecast_solar/sensor.py | 32 ++++++++----------- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 7372ac5954d..7f426d2847c 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -13,7 +13,7 @@ from homeassistant.const import ( POWER_WATT, ) -from .models import ForecastSolarSensor +from .models import ForecastSolarSensorEntityDescription DOMAIN = "forecast_solar" @@ -24,32 +24,32 @@ CONF_DAMPING = "damping" ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" -SENSORS: list[ForecastSolarSensor] = [ - ForecastSolarSensor( +SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( + ForecastSolarSensorEntityDescription( key="energy_production_today", name="Estimated Energy Production - Today", state=lambda estimate: estimate.energy_production_today / 1000, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", name="Estimated Energy Production - Tomorrow", state=lambda estimate: estimate.energy_production_tomorrow / 1000, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", name="Highest Power Peak Time - Today", device_class=DEVICE_CLASS_TIMESTAMP, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="power_highest_peak_time_tomorrow", name="Highest Power Peak Time - Tomorrow", device_class=DEVICE_CLASS_TIMESTAMP, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="power_production_now", name="Estimated Power Production - Now", device_class=DEVICE_CLASS_POWER, @@ -57,7 +57,7 @@ SENSORS: list[ForecastSolarSensor] = [ state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="power_production_next_hour", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) @@ -68,7 +68,7 @@ SENSORS: list[ForecastSolarSensor] = [ entity_registry_enabled_default=False, unit_of_measurement=POWER_WATT, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="power_production_next_12hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) @@ -79,7 +79,7 @@ SENSORS: list[ForecastSolarSensor] = [ entity_registry_enabled_default=False, unit_of_measurement=POWER_WATT, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="power_production_next_24hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) @@ -90,18 +90,18 @@ SENSORS: list[ForecastSolarSensor] = [ entity_registry_enabled_default=False, unit_of_measurement=POWER_WATT, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="energy_current_hour", name="Estimated Energy Production - This Hour", state=lambda estimate: estimate.energy_current_hour / 1000, device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), - ForecastSolarSensor( + ForecastSolarSensorEntityDescription( key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1) / 1000, name="Estimated Energy Production - Next Hour", device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), -] +) diff --git a/homeassistant/components/forecast_solar/models.py b/homeassistant/components/forecast_solar/models.py index a10f52ebcd3..6bcc97d49f2 100644 --- a/homeassistant/components/forecast_solar/models.py +++ b/homeassistant/components/forecast_solar/models.py @@ -6,16 +6,11 @@ from typing import Any, Callable from forecast_solar.models import Estimate +from homeassistant.components.sensor import SensorEntityDescription + @dataclass -class ForecastSolarSensor: - """Represents an Forecast.Solar Sensor.""" +class ForecastSolarSensorEntityDescription(SensorEntityDescription): + """Describes a Forecast.Solar Sensor.""" - key: str - name: str - - device_class: str | None = None - entity_registry_enabled_default: bool = True state: Callable[[Estimate], Any] | None = None - state_class: str | None = None - unit_of_measurement: str | None = None diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index e73b2105b8e..5d3f440f4b6 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS -from .models import ForecastSolarSensor +from .models import ForecastSolarSensorEntityDescription async def async_setup_entry( @@ -26,35 +26,31 @@ async def async_setup_entry( async_add_entities( ForecastSolarSensorEntity( - entry_id=entry.entry_id, coordinator=coordinator, sensor=sensor + entry_id=entry.entry_id, + coordinator=coordinator, + entity_description=entity_description, ) - for sensor in SENSORS + for entity_description in SENSORS ) class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): """Defines a Forcast.Solar sensor.""" + entity_description: ForecastSolarSensorEntityDescription + def __init__( self, *, entry_id: str, coordinator: DataUpdateCoordinator, - sensor: ForecastSolarSensor, + entity_description: ForecastSolarSensorEntityDescription, ) -> None: """Initialize Forcast.Solar sensor.""" super().__init__(coordinator=coordinator) - self._sensor = sensor - - self.entity_id = f"{SENSOR_DOMAIN}.{sensor.key}" - self._attr_device_class = sensor.device_class - self._attr_entity_registry_enabled_default = ( - sensor.entity_registry_enabled_default - ) - self._attr_name = sensor.name - self._attr_state_class = sensor.state_class - self._attr_unique_id = f"{entry_id}_{sensor.key}" - self._attr_unit_of_measurement = sensor.unit_of_measurement + self.entity_description = entity_description + self.entity_id = f"{SENSOR_DOMAIN}.{entity_description.key}" + self._attr_unique_id = f"{entry_id}_{entity_description.key}" self._attr_device_info = { ATTR_IDENTIFIERS: {(DOMAIN, entry_id)}, @@ -66,12 +62,12 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): @property def state(self) -> StateType: """Return the state of the sensor.""" - if self._sensor.state is None: + if self.entity_description.state is None: state: StateType | datetime = getattr( - self.coordinator.data, self._sensor.key + self.coordinator.data, self.entity_description.key ) else: - state = self._sensor.state(self.coordinator.data) + state = self.entity_description.state(self.coordinator.data) if isinstance(state, datetime): return state.isoformat() From 022ba319997b18c54eb653223c66f0ce1b655856 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 27 Jul 2021 08:53:42 -0700 Subject: [PATCH 640/818] Refactor the logic for peeking into the start of the stream (#52699) * Reset dts validator when container is reset * Reuse existing dts_validator when disabling audio stream * Refactor peek logic at the start of a stream Add a PeekingIterator to support rewinding an iterator so that the code for adjusting audio streams and start pts can be inlined in the worker. * Simplification and readability improvements * Remove unnecessary verbiage from comments and pydoc * Address pylint errors * Remove rewind function and just mux the first packet separately * More cleanup after removing rewind() * Skip check to self._buffer on every iteration --- homeassistant/components/stream/worker.py | 181 ++++++++++++---------- tests/components/stream/test_worker.py | 8 +- 2 files changed, 105 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index c4ae3f30e18..1a0a7da5c39 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -2,9 +2,8 @@ from __future__ import annotations from collections import deque -from collections.abc import Iterator, Mapping +from collections.abc import Generator, Iterator, Mapping from io import BytesIO -import itertools import logging from threading import Event from typing import Any, Callable, cast @@ -202,6 +201,48 @@ class SegmentBuffer: self._memory_file.close() +class PeekIterator(Iterator): + """An Iterator that may allow multiple passes. + + This may be consumed like a normal Iterator, however also supports a + peek() method that buffers consumed items from the iterator. + """ + + def __init__(self, iterator: Iterator[av.Packet]) -> None: + """Initialize PeekIterator.""" + self._iterator = iterator + self._buffer: deque[av.Packet] = deque() + # A pointer to either _iterator or _buffer + self._next = self._iterator.__next__ + + def __iter__(self) -> Iterator: + """Return an iterator.""" + return self + + def __next__(self) -> av.Packet: + """Return and consume the next item available.""" + return self._next() + + def _pop_buffer(self) -> av.Packet: + """Consume items from the buffer until exhausted.""" + if self._buffer: + return self._buffer.popleft() + # The buffer is empty, so change to consume from the iterator + self._next = self._iterator.__next__ + return self._next() + + def peek(self) -> Generator[av.Packet, None, None]: + """Return items without consuming from the iterator.""" + # Items consumed are added to a buffer for future calls to __next__ + # or peek. First iterate over the buffer from previous calls to peek. + self._next = self._pop_buffer + for packet in self._buffer: + yield packet + for packet in self._iterator: + self._buffer.append(packet) + yield packet + + class TimestampValidator: """Validate ordering of timestamps for packets in a stream.""" @@ -236,6 +277,31 @@ class TimestampValidator: return True +def is_keyframe(packet: av.Packet) -> Any: + """Return true if the packet is a keyframe.""" + return packet.is_keyframe + + +def unsupported_audio(packets: Iterator[av.Packet], audio_stream: Any) -> bool: + """Detect ADTS AAC, which is not supported by pyav.""" + if not audio_stream: + return False + for count, packet in enumerate(packets): + if count >= PACKETS_TO_WAIT_FOR_AUDIO: + # Some streams declare an audio stream and never send any packets + _LOGGER.warning("Audio stream not found") + break + if packet.stream == audio_stream: + # detect ADTS AAC and disable audio + if audio_stream.codec.name == "aac" and packet.size > 2: + with memoryview(packet) as packet_view: + if packet_view[0] == 0xFF and packet_view[1] & 0xF0 == 0xF0: + _LOGGER.warning("ADTS AAC detected - disabling audio stream") + return True + break + return False + + def stream_worker( source: str, options: dict[str, str], @@ -267,100 +333,55 @@ def stream_worker( if audio_stream and audio_stream.profile is None: audio_stream = None - # Iterator for demuxing - container_packets: Iterator[av.Packet] - # The video dts at the beginning of the segment - segment_start_dts: int | None = None - # Because of problems 1 and 2 below, we need to store the first few packets and replay them - initial_packets: deque[av.Packet] = deque() + dts_validator = TimestampValidator() + container_packets = PeekIterator( + filter(dts_validator.is_valid, container.demux((video_stream, audio_stream))) + ) + + def is_video(packet: av.Packet) -> Any: + """Return true if the packet is for the video stream.""" + return packet.stream == video_stream # Have to work around two problems with RTSP feeds in ffmpeg # 1 - first frame has bad pts/dts https://trac.ffmpeg.org/ticket/5018 # 2 - seeking can be problematic https://trac.ffmpeg.org/ticket/7815 - - def peek_first_dts() -> bool: - """Initialize by peeking into the first few packets of the stream. - - Deal with problem #1 above (bad first packet pts/dts) by recalculating using pts/dts from second packet. - Also load the first video keyframe dts into segment_start_dts and check if the audio stream really exists. - """ - nonlocal segment_start_dts, audio_stream, container_packets - found_audio = False - try: - # Ensure packets are ordered correctly - dts_validator = TimestampValidator() - container_packets = filter( - dts_validator.is_valid, container.demux((video_stream, audio_stream)) + # + # Use a peeking iterator to peek into the start of the stream, ensuring + # everything looks good, then go back to the start when muxing below. + try: + if audio_stream and unsupported_audio(container_packets.peek(), audio_stream): + audio_stream = None + container_packets = PeekIterator( + filter(dts_validator.is_valid, container.demux(video_stream)) ) - first_packet: av.Packet | None = None - # Get to first video keyframe - while first_packet is None: - packet = next(container_packets) - if packet.stream == audio_stream: - found_audio = True - elif packet.is_keyframe: # video_keyframe - first_packet = packet - initial_packets.append(packet) - # Get first_dts from subsequent frame to first keyframe - while segment_start_dts is None or ( - audio_stream - and not found_audio - and len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO - ): - packet = next(container_packets) - if packet.stream == audio_stream: - # detect ADTS AAC and disable audio - if audio_stream.codec.name == "aac" and packet.size > 2: - with memoryview(packet) as packet_view: - if packet_view[0] == 0xFF and packet_view[1] & 0xF0 == 0xF0: - _LOGGER.warning( - "ADTS AAC detected - disabling audio stream" - ) - container_packets = filter( - dts_validator.is_valid, - container.demux(video_stream), - ) - audio_stream = None - continue - found_audio = True - elif ( - segment_start_dts is None - ): # This is the second video frame to calculate first_dts from - segment_start_dts = packet.dts - packet.duration - first_packet.pts = first_packet.dts = segment_start_dts - initial_packets.append(packet) - if audio_stream and not found_audio: - _LOGGER.warning( - "Audio stream not found" - ) # Some streams declare an audio stream and never send any packets - except (av.AVError, StopIteration) as ex: - _LOGGER.error( - "Error demuxing stream while finding first packet: %s", str(ex) - ) - return False - return True - - if not peek_first_dts(): + # Advance to the first keyframe for muxing, then rewind so the muxing + # loop below can consume. + first_keyframe = next(filter(is_keyframe, filter(is_video, container_packets))) + # Deal with problem #1 above (bad first packet pts/dts) by recalculating + # using pts/dts from second packet. Use the peek iterator to advance + # without consuming from container_packets. Skip over the first keyframe + # then use the duration from the second video packet to adjust dts. + next_video_packet = next(filter(is_video, container_packets.peek())) + start_dts = next_video_packet.dts - next_video_packet.duration + first_keyframe.dts = first_keyframe.pts = start_dts + except (av.AVError, StopIteration) as ex: + _LOGGER.error("Error demuxing stream while finding first packet: %s", str(ex)) container.close() return segment_buffer.set_streams(video_stream, audio_stream) - assert isinstance(segment_start_dts, int) - segment_buffer.reset(segment_start_dts) + segment_buffer.reset(start_dts) + + # Mux the first keyframe, then proceed through the rest of the packets + segment_buffer.mux_packet(first_keyframe) - # Rewind the stream and iterate over the initial set of packets again - # filtering out any packets with timestamp ordering issues. - packets = itertools.chain(initial_packets, container_packets) while not quit_event.is_set(): try: - packet = next(packets) + packet = next(container_packets) except (av.AVError, StopIteration) as ex: _LOGGER.error("Error demuxing stream: %s", str(ex)) break - - # Mux packets, and possibly write a segment to the output stream. - # This mutates packet timestamps and stream segment_buffer.mux_packet(packet) # Close stream diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 793038c6770..d5f5ef653c6 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -606,10 +606,10 @@ async def test_update_stream_source(hass): nonlocal last_stream_source if not isinstance(stream_source, io.BytesIO): last_stream_source = stream_source - # Let test know the thread is running - worker_open.set() - # Block worker thread until test wakes up - worker_wake.wait() + # Let test know the thread is running + worker_open.set() + # Block worker thread until test wakes up + worker_wake.wait() return py_av.open(stream_source, args, kwargs) with patch("av.open", new=blocking_open), patch( From 6847c6dbbc665808f13eb202f8c21db797b6579e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 18:06:46 +0200 Subject: [PATCH 641/818] Use EntityDescription - rainbird (#53560) * Use EntityDescription - rainbird * Add additional type hints --- homeassistant/components/rainbird/__init__.py | 38 +++++++++++-------- .../components/rainbird/binary_sensor.py | 36 +++++++----------- homeassistant/components/rainbird/sensor.py | 35 +++++------------ 3 files changed, 47 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 55ed421bd24..6aa6e837abe 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations import logging -from typing import NamedTuple from pyrainbird import RainbirdController import voluptuous as vol from homeassistant.components import binary_sensor, sensor, switch +from homeassistant.components.binary_sensor import BinarySensorEntityDescription +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_HOST, @@ -31,24 +32,31 @@ SENSOR_TYPE_RAINDELAY = "raindelay" SENSOR_TYPE_RAINSENSOR = "rainsensor" -class RainBirdSensorMetadata(NamedTuple): - """Metadata for an individual RainBird sensor.""" - - name: str - icon: str - unit_of_measurement: str | None = None - - -SENSOR_TYPES: dict[str, RainBirdSensorMetadata] = { - SENSOR_TYPE_RAINSENSOR: RainBirdSensorMetadata( - "Rainsensor", +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=SENSOR_TYPE_RAINSENSOR, + name="Rainsensor", icon="mdi:water", ), - SENSOR_TYPE_RAINDELAY: RainBirdSensorMetadata( - "Raindelay", + SensorEntityDescription( + key=SENSOR_TYPE_RAINDELAY, + name="Raindelay", icon="mdi:water-off", ), -} +) + +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key=SENSOR_TYPE_RAINSENSOR, + name="Rainsensor", + icon="mdi:water", + ), + BinarySensorEntityDescription( + key=SENSOR_TYPE_RAINDELAY, + name="Raindelay", + icon="mdi:water-off", + ), +) TRIGGER_TIME_SCHEMA = vol.All( cv.time_period, cv.positive_timedelta, lambda td: (td.total_seconds() // 60) diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 9960d7670b2..476c2cfc8a2 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -3,15 +3,17 @@ import logging from pyrainbird import RainbirdController -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) from . import ( + BINARY_SENSOR_TYPES, DATA_RAINBIRD, RAINBIRD_CONTROLLER, SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR, - SENSOR_TYPES, - RainBirdSensorMetadata, ) _LOGGER = logging.getLogger(__name__) @@ -25,8 +27,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] add_entities( [ - RainBirdSensor(controller, sensor_type, metadata) - for sensor_type, metadata in SENSOR_TYPES.items() + RainBirdSensor(controller, description) + for description in BINARY_SENSOR_TYPES ], True, ) @@ -38,28 +40,18 @@ class RainBirdSensor(BinarySensorEntity): def __init__( self, controller: RainbirdController, - sensor_type, - metadata: RainBirdSensorMetadata, - ): + description: BinarySensorEntityDescription, + ) -> None: """Initialize the Rain Bird sensor.""" - self._sensor_type = sensor_type + self.entity_description = description self._controller = controller - self._attr_name = metadata.name - self._attr_icon = metadata.icon - self._state = None - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return None if self._state is None else bool(self._state) - - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self.name) state = None - if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + if self.entity_description.key == SENSOR_TYPE_RAINSENSOR: state = self._controller.get_rain_sensor_state() - elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + elif self.entity_description.key == SENSOR_TYPE_RAINDELAY: state = self._controller.get_rain_delay() - self._state = None if state is None else bool(state) + self._attr_is_on = None if state is None else bool(state) diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 36c3d50e1c1..2158bc5cf97 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -3,7 +3,7 @@ import logging from pyrainbird import RainbirdController -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from . import ( DATA_RAINBIRD, @@ -11,7 +11,6 @@ from . import ( SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR, SENSOR_TYPES, - RainBirdSensorMetadata, ) _LOGGER = logging.getLogger(__name__) @@ -25,10 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] add_entities( - [ - RainBirdSensor(controller, sensor_type, metadata) - for sensor_type, metadata in SENSOR_TYPES.items() - ], + [RainBirdSensor(controller, description) for description in SENSOR_TYPES], True, ) @@ -39,27 +35,16 @@ class RainBirdSensor(SensorEntity): def __init__( self, controller: RainbirdController, - sensor_type, - metadata: RainBirdSensorMetadata, - ): + description: SensorEntityDescription, + ) -> None: """Initialize the Rain Bird sensor.""" - self._sensor_type = sensor_type + self.entity_description = description self._controller = controller - self._attr_name = metadata.name - self._attr_icon = metadata.icon - self._attr_unit_of_measurement = metadata.unit_of_measurement - self._state = None - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self.name) - if self._sensor_type == SENSOR_TYPE_RAINSENSOR: - self._state = self._controller.get_rain_sensor_state() - elif self._sensor_type == SENSOR_TYPE_RAINDELAY: - self._state = self._controller.get_rain_delay() + if self.entity_description.key == SENSOR_TYPE_RAINSENSOR: + self._attr_state = self._controller.get_rain_sensor_state() + elif self.entity_description.key == SENSOR_TYPE_RAINDELAY: + self._attr_state = self._controller.get_rain_delay() From 9e9165f4ac20634272910c631946a3670669856c Mon Sep 17 00:00:00 2001 From: Johan Smits Date: Tue, 27 Jul 2021 18:30:43 +0200 Subject: [PATCH 642/818] Bump matrix-client to 0.4.0 (#53508) --- homeassistant/components/matrix/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index c28d20196e9..4e31b99c172 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -2,7 +2,7 @@ "domain": "matrix", "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", - "requirements": ["matrix-client==0.3.2"], + "requirements": ["matrix-client==0.4.0"], "codeowners": ["@tinloaf"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 94f6960cc7d..97cb5be27e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -944,7 +944,7 @@ lyft_rides==0.2 magicseaweed==1.0.3 # homeassistant.components.matrix -matrix-client==0.3.2 +matrix-client==0.4.0 # homeassistant.components.maxcube maxcube-api==0.4.3 From a1e692798fff4d3b28c3ccc37785e4b0bbd834ca Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 19:06:50 +0200 Subject: [PATCH 643/818] Use EntityDescription - ebox (#53565) --- homeassistant/components/ebox/sensor.py | 118 +++++++++++++----------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 66a5beda3d2..e27c6fe0772 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -7,13 +7,16 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import NamedTuple from pyebox import EboxClient from pyebox.client import PyEboxError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -38,81 +41,86 @@ SCAN_INTERVAL = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) -class EboxSensorMetadata(NamedTuple): - """Metadata for an individual ebox sensor.""" - - name: str - unit_of_measurement: str - icon: str - - -SENSOR_TYPES = { - "usage": EboxSensorMetadata( - "Usage", +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="usage", + name="Usage", unit_of_measurement=PERCENTAGE, icon="mdi:percent", ), - "balance": EboxSensorMetadata( - "Balance", + SensorEntityDescription( + key="balance", + name="Balance", unit_of_measurement=PRICE, icon="mdi:cash-usd", ), - "limit": EboxSensorMetadata( - "Data limit", + SensorEntityDescription( + key="limit", + name="Data limit", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), - "days_left": EboxSensorMetadata( - "Days left", + SensorEntityDescription( + key="days_left", + name="Days left", unit_of_measurement=TIME_DAYS, icon="mdi:calendar-today", ), - "before_offpeak_download": EboxSensorMetadata( - "Download before offpeak", + SensorEntityDescription( + key="before_offpeak_download", + name="Download before offpeak", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), - "before_offpeak_upload": EboxSensorMetadata( - "Upload before offpeak", + SensorEntityDescription( + key="before_offpeak_upload", + name="Upload before offpeak", unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), - "before_offpeak_total": EboxSensorMetadata( - "Total before offpeak", + SensorEntityDescription( + key="before_offpeak_total", + name="Total before offpeak", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), - "offpeak_download": EboxSensorMetadata( - "Offpeak download", + SensorEntityDescription( + key="offpeak_download", + name="Offpeak download", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), - "offpeak_upload": EboxSensorMetadata( - "Offpeak Upload", + SensorEntityDescription( + key="offpeak_upload", + name="Offpeak Upload", unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), - "offpeak_total": EboxSensorMetadata( - "Offpeak Total", + SensorEntityDescription( + key="offpeak_total", + name="Offpeak Total", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), - "download": EboxSensorMetadata( - "Download", + SensorEntityDescription( + key="download", + name="Download", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), - "upload": EboxSensorMetadata( - "Upload", + SensorEntityDescription( + key="upload", + name="Upload", unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), - "total": EboxSensorMetadata( - "Total", + SensorEntityDescription( + key="total", + name="Total", unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), -} +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -142,9 +150,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.error("Failed login: %s", exp) raise PlatformNotReady from exp - sensors = [] - for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(EBoxSensor(ebox_data, variable, name)) + sensors = [ + EBoxSensor(ebox_data, description, name) + for description in SENSOR_TYPES + if description.key in config[CONF_MONITORED_VARIABLES] + ] async_add_entities(sensors, True) @@ -152,26 +162,24 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class EBoxSensor(SensorEntity): """Implementation of a EBox sensor.""" - def __init__(self, ebox_data, sensor_type, name): + def __init__( + self, + ebox_data, + description: SensorEntityDescription, + name, + ): """Initialize the sensor.""" - self.type = sensor_type - metadata = SENSOR_TYPES[sensor_type] - self._attr_name = f"{name} {metadata.name}" - self._attr_unit_of_measurement = metadata.unit_of_measurement - self._attr_icon = metadata.icon + self.entity_description = description + self._attr_name = f"{name} {description.name}" self.ebox_data = ebox_data - self._state = None - - @property - def state(self): - """Return the state of the sensor.""" - return self._state async def async_update(self): """Get the latest data from EBox and update the state.""" await self.ebox_data.async_update() - if self.type in self.ebox_data.data: - self._state = round(self.ebox_data.data[self.type], 2) + if self.entity_description.key in self.ebox_data.data: + self._attr_state = round( + self.ebox_data.data[self.entity_description.key], 2 + ) class EBoxData: From 84a7a5fd32c15eacdab702245aebe45eb9f99b43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Jul 2021 12:10:05 -0500 Subject: [PATCH 644/818] Split color temp and color into separate HomeKit services when a light supports both (#53471) --- .../components/homekit/type_lights.py | 190 ++++++++++------ tests/components/homekit/test_type_lights.py | 208 +++++++++++++----- 2 files changed, 278 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index cb3c97fadb4..88e21272a4f 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -6,11 +6,13 @@ from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_COLOR_TEMP, DOMAIN, brightness_supported, color_supported, @@ -18,23 +20,18 @@ from homeassistant.components.light import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, STATE_ON, ) from homeassistant.core import callback -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin, - color_temperature_to_hs, -) from .accessories import TYPES, HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, + CHAR_NAME, CHAR_ON, CHAR_SATURATION, PROP_MAX_VALUE, @@ -58,59 +55,99 @@ class Light(HomeAccessory): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self.chars = [] + self.chars_primary = [] + self.chars_secondary = [] + state = self.hass.states.get(self.entity_id) + attributes = state.attributes + color_modes = attributes.get(ATTR_SUPPORTED_COLOR_MODES) + self.is_color_supported = color_supported(color_modes) + self.is_color_temp_supported = color_temp_supported(color_modes) + self.color_and_temp_supported = ( + self.is_color_supported and self.is_color_temp_supported + ) + self.is_brightness_supported = brightness_supported(color_modes) - self._features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - self._color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + if self.is_brightness_supported: + self.chars_primary.append(CHAR_BRIGHTNESS) - if brightness_supported(self._color_modes): - self.chars.append(CHAR_BRIGHTNESS) + if self.is_color_supported: + self.chars_primary.append(CHAR_HUE) + self.chars_primary.append(CHAR_SATURATION) - if color_supported(self._color_modes): - self.chars.append(CHAR_HUE) - self.chars.append(CHAR_SATURATION) - elif color_temp_supported(self._color_modes): - # ColorTemperature and Hue characteristic should not be - # exposed both. Both states are tracked separately in HomeKit, - # causing "source of truth" problems. - self.chars.append(CHAR_COLOR_TEMPERATURE) + if self.is_color_temp_supported: + if self.color_and_temp_supported: + self.chars_primary.append(CHAR_NAME) + self.chars_secondary.append(CHAR_NAME) + self.chars_secondary.append(CHAR_COLOR_TEMPERATURE) + if self.is_brightness_supported: + self.chars_secondary.append(CHAR_BRIGHTNESS) + else: + self.chars_primary.append(CHAR_COLOR_TEMPERATURE) - serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + serv_light_primary = self.add_preload_service( + SERV_LIGHTBULB, self.chars_primary + ) + serv_light_secondary = None + self.char_on_primary = serv_light_primary.configure_char(CHAR_ON, value=0) - self.char_on = serv_light.configure_char(CHAR_ON, value=0) + if self.color_and_temp_supported: + serv_light_secondary = self.add_preload_service( + SERV_LIGHTBULB, self.chars_secondary + ) + serv_light_primary.add_linked_service(serv_light_secondary) + serv_light_primary.configure_char(CHAR_NAME, value="RGB") + self.char_on_secondary = serv_light_secondary.configure_char( + CHAR_ON, value=0 + ) + serv_light_secondary.configure_char(CHAR_NAME, value="Temperature") - if CHAR_BRIGHTNESS in self.chars: + if self.is_brightness_supported: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. - self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) + self.char_brightness_primary = serv_light_primary.configure_char( + CHAR_BRIGHTNESS, value=100 + ) + if self.chars_secondary: + self.char_brightness_secondary = serv_light_secondary.configure_char( + CHAR_BRIGHTNESS, value=100 + ) - if CHAR_COLOR_TEMPERATURE in self.chars: - min_mireds = self.hass.states.get(self.entity_id).attributes.get( - ATTR_MIN_MIREDS, 153 - ) - max_mireds = self.hass.states.get(self.entity_id).attributes.get( - ATTR_MAX_MIREDS, 500 - ) + if self.is_color_temp_supported: + min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) + max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) + serv_light = serv_light_secondary or serv_light_primary self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, ) - if CHAR_HUE in self.chars: - self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) - - if CHAR_SATURATION in self.chars: - self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) + if self.is_color_supported: + self.char_hue = serv_light_primary.configure_char(CHAR_HUE, value=0) + self.char_saturation = serv_light_primary.configure_char( + CHAR_SATURATION, value=75 + ) self.async_update_state(state) - serv_light.setter_callback = self._set_chars + if self.color_and_temp_supported: + serv_light_primary.setter_callback = self._set_chars_primary + serv_light_secondary.setter_callback = self._set_chars_secondary + else: + serv_light_primary.setter_callback = self._set_chars - def _set_chars(self, char_values): - _LOGGER.debug("Light _set_chars: %s", char_values) + def _set_chars_primary(self, char_values): + """Primary service is RGB or W if only color or color temp is supported.""" + self._set_chars(char_values, True) + + def _set_chars_secondary(self, char_values): + """Secondary service is W if both color or color temp are supported.""" + self._set_chars(char_values, False) + + def _set_chars(self, char_values, is_primary=None): + _LOGGER.debug("Light _set_chars: %s, is_primary: %s", char_values, is_primary) events = [] service = SERVICE_TURN_ON params = {ATTR_ENTITY_ID: self.entity_id} @@ -127,16 +164,28 @@ class Light(HomeAccessory): params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS] events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%") - if CHAR_COLOR_TEMPERATURE in char_values: - params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] - events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}") + if service == SERVICE_TURN_OFF: + self.async_call_service( + DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id}, ", ".join(events) + ) + return - if ( - color_supported(self._color_modes) - and CHAR_HUE in char_values - and CHAR_SATURATION in char_values + if self.is_color_temp_supported and ( + is_primary is False or CHAR_COLOR_TEMPERATURE in char_values ): - color = (char_values[CHAR_HUE], char_values[CHAR_SATURATION]) + params[ATTR_COLOR_TEMP] = char_values.get( + CHAR_COLOR_TEMPERATURE, self.char_color_temperature.value + ) + events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") + + if self.is_color_supported and ( + is_primary is True + or (CHAR_HUE in char_values and CHAR_SATURATION in char_values) + ): + color = ( + char_values.get(CHAR_HUE, self.char_hue.value), + char_values.get(CHAR_SATURATION, self.char_saturation.value), + ) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color) params[ATTR_HS_COLOR] = color events.append(f"set color at {color}") @@ -148,14 +197,25 @@ class Light(HomeAccessory): """Update light after state change.""" # Handle State state = new_state.state - if state == STATE_ON and self.char_on.value != 1: - self.char_on.set_value(1) - elif state == STATE_OFF and self.char_on.value != 0: - self.char_on.set_value(0) + attributes = new_state.attributes + char_on_value = int(state == STATE_ON) + + if self.color_and_temp_supported: + color_mode = attributes.get(ATTR_COLOR_MODE) + color_temp_mode = color_mode == COLOR_MODE_COLOR_TEMP + primary_on_value = char_on_value if not color_temp_mode else 0 + secondary_on_value = char_on_value if color_temp_mode else 0 + if self.char_on_primary.value != primary_on_value: + self.char_on_primary.set_value(primary_on_value) + if self.char_on_secondary.value != secondary_on_value: + self.char_on_secondary.set_value(secondary_on_value) + else: + if self.char_on_primary.value != char_on_value: + self.char_on_primary.set_value(char_on_value) # Handle Brightness - if CHAR_BRIGHTNESS in self.chars: - brightness = new_state.attributes.get(ATTR_BRIGHTNESS) + if self.is_brightness_supported: + brightness = attributes.get(ATTR_BRIGHTNESS) if isinstance(brightness, (int, float)): brightness = round(brightness / 255 * 100, 0) # The homeassistant component might report its brightness as 0 but is @@ -170,29 +230,25 @@ class Light(HomeAccessory): # order to avoid this incorrect behavior. if brightness == 0 and state == STATE_ON: brightness = 1 - if self.char_brightness.value != brightness: - self.char_brightness.set_value(brightness) + if self.char_brightness_primary.value != brightness: + self.char_brightness_primary.set_value(brightness) + if ( + self.color_and_temp_supported + and self.char_brightness_secondary.value != brightness + ): + self.char_brightness_secondary.set_value(brightness) # Handle color temperature - if CHAR_COLOR_TEMPERATURE in self.chars: - color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) + if self.is_color_temp_supported: + color_temperature = attributes.get(ATTR_COLOR_TEMP) if isinstance(color_temperature, (int, float)): color_temperature = round(color_temperature, 0) if self.char_color_temperature.value != color_temperature: self.char_color_temperature.set_value(color_temperature) # Handle Color - if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - if ATTR_HS_COLOR in new_state.attributes: - hue, saturation = new_state.attributes[ATTR_HS_COLOR] - elif ATTR_COLOR_TEMP in new_state.attributes: - hue, saturation = color_temperature_to_hs( - color_temperature_mired_to_kelvin( - new_state.attributes[ATTR_COLOR_TEMP] - ) - ) - else: - hue, saturation = None, None + if self.is_color_supported: + hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): hue = round(hue, 0) saturation = round(saturation, 0) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 53d6ee02be6..f75e6bf19ac 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -8,9 +8,14 @@ from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_XY, DOMAIN, ) from homeassistant.const import ( @@ -39,40 +44,44 @@ async def test_light_basic(hass, hk_driver, events): assert acc.aid == 1 assert acc.category == 5 # Lightbulb - assert acc.char_on.value + assert acc.char_on_primary.value await acc.run() await hass.async_block_till_done() - assert acc.char_on.value == 1 + assert acc.char_on_primary.value == 1 hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - assert acc.char_on.value == 0 + assert acc.char_on_primary.value == 0 hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() - assert acc.char_on.value == 0 + assert acc.char_on_primary.value == 0 hass.states.async_remove(entity_id) await hass.async_block_till_done() - assert acc.char_on.value == 0 + assert acc.char_on_primary.value == 0 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") - char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1} + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 1, + } ] }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_on.client_update_value, 1) + await hass.async_add_executor_job(acc.char_on_primary.client_update_value, 1) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id @@ -85,7 +94,11 @@ async def test_light_basic(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0} + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 0, + } ] }, "mock_addr", @@ -115,17 +128,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness.value != 0 - char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] - char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness_primary.value != 0 + char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] + char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness.value == 100 + assert acc.char_brightness_primary.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness.value == 40 + assert acc.char_brightness_primary.value == 40 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -134,10 +147,14 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_iid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 1, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_primary_iid, HAP_REPR_VALUE: 20, }, ] @@ -156,10 +173,14 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_iid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 1, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_primary_iid, HAP_REPR_VALUE: 40, }, ] @@ -178,10 +199,14 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_iid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 1, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_primary_iid, HAP_REPR_VALUE: 0, }, ] @@ -198,24 +223,24 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # in update_state hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness.value == 1 + assert acc.char_brightness_primary.value == 1 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - assert acc.char_brightness.value == 100 + assert acc.char_brightness_primary.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness.value == 1 + assert acc.char_brightness_primary.value == 1 # Ensure floats are handled hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66}) await hass.async_block_till_done() - assert acc.char_brightness.value == 22 + assert acc.char_brightness_primary.value == 22 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4}) await hass.async_block_till_done() - assert acc.char_brightness.value == 43 + assert acc.char_brightness_primary.value == 43 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0}) await hass.async_block_till_done() - assert acc.char_brightness.value == 1 + assert acc.char_brightness_primary.value == 1 async def test_light_color_temperature(hass, hk_driver, events): @@ -266,7 +291,12 @@ async def test_light_color_temperature(hass, hk_driver, events): @pytest.mark.parametrize( - "supported_color_modes", [["ct", "hs"], ["ct", "rgb"], ["ct", "xy"]] + "supported_color_modes", + [ + [COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS], + [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB], + [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], + ], ) async def test_light_color_temperature_and_rgb_color( hass, hk_driver, events, supported_color_modes @@ -280,29 +310,93 @@ async def test_light_color_temperature_and_rgb_color( { ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_COLOR_TEMP: 190, + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGB, ATTR_HS_COLOR: (260, 90), }, ) await hass.async_block_till_done() - acc = Light(hass, hk_driver, "Light", entity_id, 2, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) assert acc.char_hue.value == 260 assert acc.char_saturation.value == 90 + assert acc.char_on_primary.value == 1 + assert acc.char_on_secondary.value == 0 + assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness_secondary.value == 100 - assert not hasattr(acc, "char_color_temperature") + assert hasattr(acc, "char_color_temperature") - hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_COLOR_TEMP: 224, + ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, + ATTR_BRIGHTNESS: 127, + }, + ) await hass.async_block_till_done() await acc.run() await hass.async_block_till_done() - assert acc.char_hue.value == 27 - assert acc.char_saturation.value == 27 + assert acc.char_color_temperature.value == 224 + assert acc.char_on_primary.value == 0 + assert acc.char_on_secondary.value == 1 + assert acc.char_brightness_primary.value == 50 + assert acc.char_brightness_secondary.value == 50 - hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_COLOR_TEMP: 352, + ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, + }, + ) await hass.async_block_till_done() await acc.run() await hass.async_block_till_done() - assert acc.char_hue.value == 28 - assert acc.char_saturation.value == 61 + assert acc.char_color_temperature.value == 352 + assert acc.char_on_primary.value == 0 + assert acc.char_on_secondary.value == 1 + hk_driver.add_accessory(acc) + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + assert acc.char_hue.value == 145 + assert acc.char_saturation.value == 75 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_VALUE: 200, + }, + ] + }, + "mock_addr", + ) + assert acc.char_color_temperature.value == 200 @pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) @@ -382,13 +476,13 @@ async def test_light_restore(hass, hk_driver, events): hk_driver.add_accessory(acc) assert acc.category == 5 # Lightbulb - assert acc.chars == [] - assert acc.char_on.value == 0 + assert acc.chars_primary == [] + assert acc.char_on_primary.value == 0 acc = Light(hass, hk_driver, "Light", "light.all_info_set", 2, None) assert acc.category == 5 # Lightbulb - assert acc.chars == ["Brightness"] - assert acc.char_on.value == 0 + assert acc.chars_primary == ["Brightness"] + assert acc.char_on_primary.value == 0 async def test_light_set_brightness_and_color(hass, hk_driver, events): @@ -409,19 +503,19 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness.value != 0 - char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] - char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness_primary.value != 0 + char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] + char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness.value == 100 + assert acc.char_brightness_primary.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness.value == 40 + assert acc.char_brightness_primary.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)}) await hass.async_block_till_done() @@ -434,10 +528,14 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_iid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 1, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_primary_iid, HAP_REPR_VALUE: 20, }, { @@ -485,18 +583,18 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness.value != 0 - char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] - char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness_primary.value != 0 + char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] + char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness.value == 100 + assert acc.char_brightness_primary.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness.value == 40 + assert acc.char_brightness_primary.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)}) await hass.async_block_till_done() @@ -508,10 +606,14 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_iid, + HAP_REPR_IID: char_on_primary_iid, + HAP_REPR_VALUE: 1, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_primary_iid, HAP_REPR_VALUE: 20, }, { From 9607864c295f260989cc052427a144f2e567ad60 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 27 Jul 2021 19:10:38 +0200 Subject: [PATCH 645/818] Bump `gios` library to version 2.0 (#53557) * Bump gios library * Fix pylint error --- homeassistant/components/gios/config_flow.py | 1 + homeassistant/components/gios/const.py | 2 +- homeassistant/components/gios/manifest.json | 2 +- homeassistant/components/gios/sensor.py | 56 ++++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gios/test_sensor.py | 36 +++++++++++-- 7 files changed, 77 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index d2bd3968d10..ff3f33408a5 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -41,6 +41,7 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): gios = Gios(user_input[CONF_STATION_ID], websession) await gios.async_update() + assert gios.station_name is not None return self.async_create_entry( title=gios.station_name, data=user_input, diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index f78e876a0e7..834735c6189 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -31,7 +31,7 @@ ATTR_CO: Final = "co" ATTR_NO2: Final = "no2" ATTR_O3: Final = "o3" ATTR_PM10: Final = "pm10" -ATTR_PM25: Final = "pm2.5" +ATTR_PM25: Final = "pm25" ATTR_SO2: Final = "so2" ATTR_AQI: Final = "aqi" diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index f13da0e3f33..3e7bf9aceca 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==1.0.2"], + "requirements": ["gios==2.0.0"], "config_flow": true, "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index b9c1290dc00..4ab7facec9f 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -1,13 +1,19 @@ """Support for the GIOS service.""" from __future__ import annotations +import logging from typing import Any, cast -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as PLATFORM, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -15,6 +21,7 @@ from . import GiosDataUpdateCoordinator from .const import ( ATTR_AQI, ATTR_INDEX, + ATTR_PM25, ATTR_STATION, ATTR_UNIT, ATTR_VALUE, @@ -25,6 +32,8 @@ from .const import ( SENSOR_TYPES, ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -34,10 +43,26 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id] + # Due to the change of the attribute name of one sensor, it is necessary to migrate + # the unique_id to the new name. + entity_registry = await async_get_registry(hass) + old_unique_id = f"{coordinator.gios.station_id}-pm2.5" + if entity_id := entity_registry.async_get_entity_id( + PLATFORM, DOMAIN, old_unique_id + ): + new_unique_id = f"{coordinator.gios.station_id}-{ATTR_PM25}" + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + sensors: list[GiosSensor | GiosAqiSensor] = [] - for sensor, sensor_data in coordinator.data.items(): - if sensor not in SENSOR_TYPES or not sensor_data.get(ATTR_VALUE): + for sensor in SENSOR_TYPES: + if getattr(coordinator.data, sensor) is None: continue if sensor == ATTR_AQI: sensors.append(GiosAqiSensor(name, sensor, coordinator)) @@ -64,12 +89,14 @@ class GiosSensor(CoordinatorEntity, SensorEntity): "entry_type": "service", } self._attr_icon = "mdi:blur" - self._attr_name = f"{name} {sensor_type.upper()}" + if sensor_type == ATTR_PM25: + self._attr_name = f"{name} PM2.5" + else: + self._attr_name = f"{name} {sensor_type.upper()}" self._attr_state_class = self._description.get(ATTR_STATE_CLASS) self._attr_unique_id = f"{coordinator.gios.station_id}-{sensor_type}" self._attr_unit_of_measurement = self._description.get(ATTR_UNIT) self._sensor_type = sensor_type - self._state = None self._attrs: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.coordinator.gios.station_name, @@ -78,18 +105,17 @@ class GiosSensor(CoordinatorEntity, SensorEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.coordinator.data[self._sensor_type].get(ATTR_INDEX): - self._attrs[ATTR_NAME] = self.coordinator.data[self._sensor_type][ATTR_NAME] - self._attrs[ATTR_INDEX] = self.coordinator.data[self._sensor_type][ - ATTR_INDEX - ] + self._attrs[ATTR_NAME] = getattr(self.coordinator.data, self._sensor_type).name + self._attrs[ATTR_INDEX] = getattr( + self.coordinator.data, self._sensor_type + ).index return self._attrs @property def state(self) -> StateType: """Return the state.""" - self._state = self.coordinator.data[self._sensor_type][ATTR_VALUE] - return cast(StateType, self._description[ATTR_VALUE](self._state)) + state = getattr(self.coordinator.data, self._sensor_type).value + return cast(StateType, self._description[ATTR_VALUE](state)) class GiosAqiSensor(GiosSensor): @@ -98,12 +124,10 @@ class GiosAqiSensor(GiosSensor): @property def state(self) -> StateType: """Return the state.""" - return cast(StateType, self.coordinator.data[self._sensor_type][ATTR_VALUE]) + return cast(StateType, getattr(self.coordinator.data, self._sensor_type).value) @property def available(self) -> bool: """Return if entity is available.""" available = super().available - return available and bool( - self.coordinator.data[self._sensor_type].get(ATTR_VALUE) - ) + return available and bool(getattr(self.coordinator.data, self._sensor_type)) diff --git a/requirements_all.txt b/requirements_all.txt index 97cb5be27e4..38d7bd57c7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -683,7 +683,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==1.0.2 +gios==2.0.0 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26123a6a672..36de2c46daf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==1.0.2 +gios==2.0.0 # homeassistant.components.glances glances_api==0.2.0 diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 8ce192b7e9c..2da3d8e1e8c 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -5,8 +5,17 @@ from unittest.mock import patch from gios import ApiError -from homeassistant.components.gios.const import ATTR_INDEX, ATTR_STATION, ATTRIBUTION -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.gios.const import ( + ATTR_INDEX, + ATTR_STATION, + ATTRIBUTION, + DOMAIN, +) +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as PLATFORM, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, @@ -126,7 +135,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_pm2_5") assert entry - assert entry.unique_id == "123-pm2.5" + assert entry.unique_id == "123-pm25" state = hass.states.get("sensor.home_so2") assert state @@ -306,7 +315,7 @@ async def test_invalid_indexes(hass): entry = registry.async_get("sensor.home_pm2_5") assert entry - assert entry.unique_id == "123-pm2.5" + assert entry.unique_id == "123-pm25" state = hass.states.get("sensor.home_so2") assert state @@ -352,3 +361,22 @@ async def test_aqi_sensor_availability(hass): state = hass.states.get("sensor.home_aqi") assert state assert state.state == STATE_UNAVAILABLE + + +async def test_unique_id_migration(hass): + """Test states of the unique_id migration.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + PLATFORM, + DOMAIN, + "123-pm2.5", + suggested_object_id="home_pm2_5", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("sensor.home_pm2_5") + assert entry + assert entry.unique_id == "123-pm25" From 64a3c669ceeb28e4a93df4b01ed2bddf7571b0c7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 19:12:17 +0200 Subject: [PATCH 646/818] Use EntityDescription - rova (#53531) * Use EntityDescription - rova * Fix pylint protected-access * Changes after review --- homeassistant/components/rova/sensor.py | 83 +++++++++---------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 40dab258954..35d8c0ae2c0 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -3,13 +3,16 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import NamedTuple from requests.exceptions import ConnectTimeout, HTTPError from rova.rova import Rova import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, @@ -27,33 +30,25 @@ UPDATE_DELAY = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12) -class RovaSensorMetadata(NamedTuple): - """Metadata for an individual rova sensor.""" - - name: str - json_key: str - icon: str - - -SENSOR_TYPES: dict[str, RovaSensorMetadata] = { - "bio": RovaSensorMetadata( - "Biowaste", - json_key="gft", +SENSOR_TYPES: dict[str, SensorEntityDescription] = { + "bio": SensorEntityDescription( + key="gft", + name="bio", icon="mdi:recycle", ), - "paper": RovaSensorMetadata( - "Paper", - json_key="papier", + "paper": SensorEntityDescription( + key="papier", + name="paper", icon="mdi:recycle", ), - "plastic": RovaSensorMetadata( - "PET", - json_key="pmd", + "plastic": SensorEntityDescription( + key="pmd", + name="plastic", icon="mdi:recycle", ), - "residual": RovaSensorMetadata( - "Residual", - json_key="restafval", + "residual": SensorEntityDescription( + key="restafval", + name="residual", icon="mdi:recycle", ), } @@ -96,50 +91,32 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_service = RovaData(api) # Create a new sensor for each garbage type. - entities = [] - for sensor_key in config[CONF_MONITORED_CONDITIONS]: - sensor = RovaSensor(platform_name, sensor_key, data_service) - entities.append(sensor) - + entities = [ + RovaSensor(platform_name, SENSOR_TYPES[sensor_key], data_service) + for sensor_key in config[CONF_MONITORED_CONDITIONS] + ] add_entities(entities, True) class RovaSensor(SensorEntity): """Representation of a Rova sensor.""" - def __init__(self, platform_name, sensor_key, data_service): + def __init__( + self, platform_name, description: SensorEntityDescription, data_service + ): """Initialize the sensor.""" - self.sensor_key = sensor_key - self.platform_name = platform_name + self.entity_description = description self.data_service = data_service - self._state = None - - metadata = SENSOR_TYPES[sensor_key] - self._json_key = metadata.json_key - self._attr_icon = metadata.icon - - @property - def name(self): - """Return the name.""" - return f"{self.platform_name}_{self.sensor_key}" - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def state(self): - """Return the state of the sensor.""" - return self._state + self._attr_name = f"{platform_name}_{description.name}" + self._attr_device_class = DEVICE_CLASS_TIMESTAMP def update(self): """Get the latest data from the sensor and update the state.""" self.data_service.update() - pickup_date = self.data_service.data.get(self._json_key) + pickup_date = self.data_service.data.get(self.entity_description.key) if pickup_date is not None: - self._state = pickup_date.isoformat() + self._attr_state = pickup_date.isoformat() class RovaData: From ce663f629c708c245e332b7137cb049938ab8adc Mon Sep 17 00:00:00 2001 From: Yuxiang Zhu Date: Wed, 28 Jul 2021 01:31:51 +0800 Subject: [PATCH 647/818] Fix various zeroconf IPv6 compatibility issues (#53505) --- homeassistant/components/zeroconf/__init__.py | 52 +++++++++++++--- tests/components/zeroconf/test_init.py | 61 +++++++++++++++++-- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a19da8df75f..4c4c81aff32 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -150,14 +150,21 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: if not adapter["enabled"]: continue if ipv4s := adapter["ipv4"]: - interfaces.append(ipv4s[0]["address"]) - elif ipv6s := adapter["ipv6"]: - interfaces.append(ipv6s[0]["scope_id"]) + interfaces.extend( + ipv4["address"] + for ipv4 in ipv4s + if not ipaddress.ip_address(ipv4["address"]).is_loopback + ) + if adapter["ipv6"]: + ifi = socket.if_nametoindex(adapter["name"]) + interfaces.append(ifi) ipv6 = True if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): ipv6 = False zc_args["ip_version"] = IPVersion.V4Only + else: + zc_args["ip_version"] = IPVersion.All aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) @@ -190,6 +197,32 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True +def _get_announced_addresses( + adapters: list[Adapter], + first_ip: bytes | None = None, +) -> list[bytes]: + """Return a list of IP addresses to announce via zeroconf. + + If first_ip is not None, it will be the first address in the list. + """ + addresses = { + addr.packed + for addr in [ + ipaddress.ip_address(ip["address"]) + for adapter in adapters + if adapter["enabled"] + for ip in cast(list, adapter["ipv6"]) + cast(list, adapter["ipv4"]) + ] + if not (addr.is_unspecified or addr.is_loopback) + } + if first_ip: + address_list = [first_ip] + address_list.extend(addresses - set({first_ip})) + else: + address_list = list(addresses) + return address_list + + async def _async_register_hass_zc_service( hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str ) -> None: @@ -218,12 +251,15 @@ async def _async_register_hass_zc_service( # Set old base URL based on external or internal params["base_url"] = params["external_url"] or params["internal_url"] - host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) + adapters = await network.async_get_adapters(hass) - try: + # Puts the default IPv4 address first in the list to preserve compatibility, + # because some mDNS implementations ignores anything but the first announced address. + host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) + host_ip_pton = None + if host_ip: host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) - except OSError: - host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) + address_list = _get_announced_addresses(adapters, host_ip_pton) _suppress_invalid_properties(params) @@ -231,7 +267,7 @@ async def _async_register_hass_zc_service( ZEROCONF_TYPE, name=f"{valid_location_name}.{ZEROCONF_TYPE}", server=f"{uuid}.local.", - addresses=[host_ip_pton], + addresses=address_list, port=hass.http.server_port, properties=params, ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 68c0785e60b..3b8cf883a13 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,11 +1,16 @@ """Test Zeroconf component setup process.""" +from ipaddress import ip_address from unittest.mock import call, patch from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange from zeroconf.asyncio import AsyncServiceInfo from homeassistant.components import zeroconf -from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 +from homeassistant.components.zeroconf import ( + CONF_DEFAULT_INTERFACE, + CONF_IPV6, + _get_announced_addresses, +) from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, @@ -726,10 +731,16 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ "ipv6": [ { "address": "2001:db8::", - "network_prefix": 8, + "network_prefix": 64, "flowinfo": 1, "scope_id": 1, - } + }, + { + "address": "fe80::1234:5678:9abc:def0", + "network_prefix": 64, + "flowinfo": 1, + "scope_id": 1, + }, ], "name": "eth0", }, @@ -741,6 +752,21 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ "ipv6": [], "name": "eth1", }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "172.16.1.5", "network_prefix": 23}], + "ipv6": [ + { + "address": "fe80::dead:beef:dead:beef", + "network_prefix": 64, + "flowinfo": 1, + "scope_id": 3, + } + ], + "name": "eth2", + }, { "auto": False, "default": False, @@ -764,9 +790,36 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero ), patch( "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, + ), patch( + "socket.if_nametoindex", + side_effect=lambda iface: {"eth0": 1, "eth1": 2, "eth2": 3, "vtun0": 4}.get( + iface, 0 + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + assert mock_zc.mock_calls[0] == call( + interfaces=[1, "192.168.1.5", "172.16.1.5", 3], ip_version=IPVersion.All + ) - assert mock_zc.mock_calls[0] == call(interfaces=[1, "192.168.1.5"]) + +async def test_get_announced_addresses(hass, mock_async_zeroconf): + """Test addresses for mDNS announcement.""" + expected = { + ip_address(ip).packed + for ip in [ + "fe80::1234:5678:9abc:def0", + "2001:db8::", + "192.168.1.5", + "fe80::dead:beef:dead:beef", + "172.16.1.5", + ] + } + first_ip = ip_address("172.16.1.5").packed + actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip) + assert actual[0] == first_ip and set(actual) == expected + + first_ip = ip_address("192.168.1.5").packed + actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip) + assert actual[0] == first_ip and set(actual) == expected From 27d42e0cd839169571a3774e4c0be25064e0f4e3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 27 Jul 2021 19:36:46 +0200 Subject: [PATCH 648/818] KNX: Support for HS-color lights (#53538) --- homeassistant/components/knx/light.py | 25 ++++++++++++ homeassistant/components/knx/schema.py | 55 +++++++++++++++++++++----- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 56068b5deae..b807ad1335d 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -10,11 +10,13 @@ from xknx.telegram.address import parse_device_group_address from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, @@ -158,6 +160,12 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS), group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS), group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS), + group_address_hue=config.get(LightSchema.CONF_HUE_ADDRESS), + group_address_hue_state=config.get(LightSchema.CONF_HUE_STATE_ADDRESS), + group_address_saturation=config.get(LightSchema.CONF_SATURATION_ADDRESS), + group_address_saturation_state=config.get( + LightSchema.CONF_SATURATION_STATE_ADDRESS + ), group_address_xyy_color=config.get(LightSchema.CONF_XYY_ADDRESS), group_address_xyy_color_state=config.get(LightSchema.CONF_XYY_STATE_ADDRESS), group_address_tunable_white=group_address_tunable_white, @@ -283,6 +291,13 @@ class KNXLight(KnxEntity, LightEntity): return (*rgb, white) return None + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the hue and saturation color value [float, float].""" + # Hue is scaled 0..360 int encoded in 1 byte in KNX (-> only 256 possible values) + # Saturation is scaled 0..100 int + return self._device.current_hs_color + @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color value [float, float].""" @@ -315,6 +330,8 @@ class KNXLight(KnxEntity, LightEntity): """Return the color mode of the light.""" if self._device.supports_xyy_color: return COLOR_MODE_XY + if self._device.supports_hs_color: + return COLOR_MODE_HS if self._device.supports_rgbw: return COLOR_MODE_RGBW if self._device.supports_color: @@ -339,6 +356,7 @@ class KNXLight(KnxEntity, LightEntity): mireds = kwargs.get(ATTR_COLOR_TEMP) rgb = kwargs.get(ATTR_RGB_COLOR) rgbw = kwargs.get(ATTR_RGBW_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) xy_color = kwargs.get(ATTR_XY_COLOR) if ( @@ -347,6 +365,7 @@ class KNXLight(KnxEntity, LightEntity): and mireds is None and rgb is None and rgbw is None + and hs_color is None and xy_color is None ): await self._device.set_on() @@ -396,6 +415,12 @@ class KNXLight(KnxEntity, LightEntity): ) return + if hs_color is not None: + # round so only one telegram will be sent if the other matches state + hue = round(hs_color[0]) + sat = round(hs_color[1]) + await self._device.set_hs_color((hue, sat)) + if brightness is not None: # brightness: 1..255; 0 brightness will call async_turn_off() if self._device.brightness.writable: diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 079dc7363bf..11b2504d129 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -496,8 +496,12 @@ class LightSchema(KNXPlatformSchema): CONF_COLOR_TEMP_ADDRESS = "color_temperature_address" CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address" CONF_COLOR_TEMP_MODE = "color_temperature_mode" + CONF_HUE_ADDRESS = "hue_address" + CONF_HUE_STATE_ADDRESS = "hue_state_address" CONF_RGBW_ADDRESS = "rgbw_address" CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" + CONF_SATURATION_ADDRESS = "saturation_address" + CONF_SATURATION_STATE_ADDRESS = "saturation_state_address" CONF_XYY_ADDRESS = "xyy_address" CONF_XYY_STATE_ADDRESS = "xyy_state_address" CONF_MIN_KELVIN = "min_kelvin" @@ -514,7 +518,18 @@ class LightSchema(KNXPlatformSchema): CONF_BLUE = "blue" CONF_WHITE = "white" - COLOR_SCHEMA = vol.Schema( + _hs_color_inclusion_msg = ( + "'hue_address', 'saturation_address' and 'brightness_address'" + " are required for hs_color configuration" + ) + HS_COLOR_SCHEMA = { + vol.Optional(CONF_HUE_ADDRESS): ga_list_validator, + vol.Optional(CONF_HUE_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_SATURATION_ADDRESS): ga_list_validator, + vol.Optional(CONF_SATURATION_STATE_ADDRESS): ga_list_validator, + } + + INDIVIDUAL_COLOR_SCHEMA = vol.Schema( { vol.Optional(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, @@ -536,18 +551,18 @@ class LightSchema(KNXPlatformSchema): CONF_RED, "individual_colors", msg="'red', 'green' and 'blue' are required for individual colors configuration", - ): COLOR_SCHEMA, + ): INDIVIDUAL_COLOR_SCHEMA, vol.Inclusive( CONF_GREEN, "individual_colors", msg="'red', 'green' and 'blue' are required for individual colors configuration", - ): COLOR_SCHEMA, + ): INDIVIDUAL_COLOR_SCHEMA, vol.Inclusive( CONF_BLUE, "individual_colors", msg="'red', 'green' and 'blue' are required for individual colors configuration", - ): COLOR_SCHEMA, - vol.Optional(CONF_WHITE): COLOR_SCHEMA, + ): INDIVIDUAL_COLOR_SCHEMA, + vol.Optional(CONF_WHITE): INDIVIDUAL_COLOR_SCHEMA, }, vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_list_validator, vol.Optional(CONF_COLOR_STATE_ADDRESS): ga_list_validator, @@ -556,6 +571,7 @@ class LightSchema(KNXPlatformSchema): vol.Optional( CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE ): vol.All(vol.Upper, cv.enum(ColorTempModes)), + **HS_COLOR_SCHEMA, vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_list_validator, vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_list_validator, vol.Exclusive(CONF_XYY_ADDRESS, "color"): ga_list_validator, @@ -569,20 +585,39 @@ class LightSchema(KNXPlatformSchema): } ), vol.Any( - # either global "address" or "individual_colors" is required vol.Schema( + {vol.Required(KNX_ADDRESS): object}, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( # brightness addresses are required in INDIVIDUAL_COLOR_SCHEMA + {vol.Required(CONF_INDIVIDUAL_COLORS): object}, + extra=vol.ALLOW_EXTRA, + ), + msg="either 'address' or 'individual_colors' is required", + ), + vol.Any( + vol.Schema( # 'brightness' is non-optional for hs-color { - # brightness addresses are required in COLOR_SCHEMA - vol.Required(CONF_INDIVIDUAL_COLORS): object, + vol.Inclusive( + CONF_BRIGHTNESS_ADDRESS, "hs_color", msg=_hs_color_inclusion_msg + ): object, + vol.Inclusive( + CONF_HUE_ADDRESS, "hs_color", msg=_hs_color_inclusion_msg + ): object, + vol.Inclusive( + CONF_SATURATION_ADDRESS, "hs_color", msg=_hs_color_inclusion_msg + ): object, }, extra=vol.ALLOW_EXTRA, ), - vol.Schema( + vol.Schema( # hs-colors not used { - vol.Required(KNX_ADDRESS): object, + vol.Optional(CONF_HUE_ADDRESS): None, + vol.Optional(CONF_SATURATION_ADDRESS): None, }, extra=vol.ALLOW_EXTRA, ), + msg=_hs_color_inclusion_msg, ), ) From a133eae88e222af1a2b74ac904c98638972f79f7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 Jul 2021 19:48:37 +0200 Subject: [PATCH 649/818] Add more mysensors sensor attributes (#53566) --- homeassistant/components/mysensors/sensor.py | 117 +++++++++++++----- homeassistant/components/mysensors/switch.py | 13 -- tests/components/mysensors/conftest.py | 14 +++ tests/components/mysensors/test_sensor.py | 21 ++++ .../mysensors/power_sensor_state.json | 21 ++++ 5 files changed, 141 insertions(+), 45 deletions(-) create mode 100644 tests/fixtures/mysensors/power_sensor_state.json diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index f2908567a14..c7755b13512 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,16 +1,27 @@ """Support for MySensors sensors.""" from __future__ import annotations +from datetime import datetime + from awesomeversion import AwesomeVersion from homeassistant.components import mysensors -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONDUCTIVITY, DEGREE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, ELECTRIC_POTENTIAL_VOLT, @@ -30,42 +41,68 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.dt import utc_from_timestamp from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { - "V_TEMP": [None, None, DEVICE_CLASS_TEMPERATURE], - "V_HUM": [PERCENTAGE, "mdi:water-percent", DEVICE_CLASS_HUMIDITY], - "V_DIMMER": [PERCENTAGE, "mdi:percent", None], - "V_PERCENTAGE": [PERCENTAGE, "mdi:percent", None], - "V_PRESSURE": [None, "mdi:gauge", None], - "V_FORECAST": [None, "mdi:weather-partly-cloudy", None], - "V_RAIN": [None, "mdi:weather-rainy", None], - "V_RAINRATE": [None, "mdi:weather-rainy", None], - "V_WIND": [None, "mdi:weather-windy", None], - "V_GUST": [None, "mdi:weather-windy", None], - "V_DIRECTION": [DEGREE, "mdi:compass", None], - "V_WEIGHT": [MASS_KILOGRAMS, "mdi:weight-kilogram", None], - "V_DISTANCE": [LENGTH_METERS, "mdi:ruler", None], - "V_IMPEDANCE": ["ohm", None, None], - "V_WATT": [POWER_WATT, None, None], - "V_KWH": [ENERGY_KILO_WATT_HOUR, None, None], - "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny", None], - "V_FLOW": [LENGTH_METERS, "mdi:gauge", None], - "V_VOLUME": [f"{VOLUME_CUBIC_METERS}", None, None], + "V_TEMP": [None, None, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT], + "V_HUM": [ + PERCENTAGE, + "mdi:water-percent", + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + ], + "V_DIMMER": [PERCENTAGE, "mdi:percent", None, None], + "V_PERCENTAGE": [PERCENTAGE, "mdi:percent", None, None], + "V_PRESSURE": [None, "mdi:gauge", None, None], + "V_FORECAST": [None, "mdi:weather-partly-cloudy", None, None], + "V_RAIN": [None, "mdi:weather-rainy", None, None], + "V_RAINRATE": [None, "mdi:weather-rainy", None, None], + "V_WIND": [None, "mdi:weather-windy", None, None], + "V_GUST": [None, "mdi:weather-windy", None, None], + "V_DIRECTION": [DEGREE, "mdi:compass", None, None], + "V_WEIGHT": [MASS_KILOGRAMS, "mdi:weight-kilogram", None, None], + "V_DISTANCE": [LENGTH_METERS, "mdi:ruler", None, None], + "V_IMPEDANCE": ["ohm", None, None, None], + "V_WATT": [POWER_WATT, None, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT], + "V_KWH": [ + ENERGY_KILO_WATT_HOUR, + None, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + ], + "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny", None, None], + "V_FLOW": [LENGTH_METERS, "mdi:gauge", None, None], + "V_VOLUME": [VOLUME_CUBIC_METERS, None, None, None], "V_LEVEL": { - "S_SOUND": [SOUND_PRESSURE_DB, "mdi:volume-high", None], - "S_VIBRATION": [FREQUENCY_HERTZ, None, None], - "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny", None], + "S_SOUND": [SOUND_PRESSURE_DB, "mdi:volume-high", None, None], + "S_VIBRATION": [FREQUENCY_HERTZ, None, None, None], + "S_LIGHT_LEVEL": [ + LIGHT_LUX, + "mdi:white-balance-sunny", + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + ], }, - "V_VOLTAGE": [ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], - "V_CURRENT": [ELECTRIC_CURRENT_AMPERE, "mdi:flash-auto", None], - "V_PH": ["pH", None, None], - "V_ORP": [ELECTRIC_POTENTIAL_MILLIVOLT, None, None], - "V_EC": [CONDUCTIVITY, None, None], - "V_VAR": ["var", None, None], - "V_VA": [POWER_VOLT_AMPERE, None, None], + "V_VOLTAGE": [ + ELECTRIC_POTENTIAL_VOLT, + "mdi:flash", + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ], + "V_CURRENT": [ + ELECTRIC_CURRENT_AMPERE, + "mdi:flash-auto", + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + ], + "V_PH": ["pH", None, None, None], + "V_ORP": [ELECTRIC_POTENTIAL_MILLIVOLT, None, None, None], + "V_EC": [CONDUCTIVITY, None, None, None], + "V_VAR": ["var", None, None, None], + "V_VA": [POWER_VOLT_AMPERE, None, None, None], } @@ -124,6 +161,20 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Return the icon to use in the frontend, if any.""" return self._get_sensor_type()[1] + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + set_req = self.gateway.const.SetReq + + if set_req(self.value_type).name == "V_KWH": + return utc_from_timestamp(0) + return None + + @property + def state_class(self) -> str | None: + """Return the state class of this entity.""" + return self._get_sensor_type()[3] + @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" @@ -148,10 +199,12 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq - _sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None, None]) + _sensor_type = SENSORS.get( + set_req(self.value_type).name, [None, None, None, None] + ) if isinstance(_sensor_type, dict): sensor_type = _sensor_type.get( - pres(self.child_type).name, [None, None, None] + pres(self.child_type).name, [None, None, None, None] ) else: sensor_type = _sensor_type diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index cdb4979d16b..8f8c759c364 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,7 +1,6 @@ """Support for MySensors switches.""" from __future__ import annotations -from contextlib import suppress from typing import Any import voluptuous as vol @@ -109,18 +108,6 @@ async def async_setup_entry( class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): """Representation of the value of a MySensors Switch child node.""" - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - set_req = self.gateway.const.SetReq - value = self._values.get(set_req.V_WATT) - float_value: float | None = None - if value is not None: - with suppress(ValueError): - float_value = float(value) - - return float_value - @property def is_on(self) -> bool: """Return True if switch is on.""" diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 8fbe9486352..49c32301442 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -156,3 +156,17 @@ def gps_sensor(gateway_nodes, gps_sensor_state) -> Sensor: nodes = update_gateway_nodes(gateway_nodes, gps_sensor_state) node = nodes[1] return node + + +@pytest.fixture(name="power_sensor_state", scope="session") +def power_sensor_state_fixture() -> dict: + """Load the power sensor state.""" + return load_nodes_state("mysensors/power_sensor_state.json") + + +@pytest.fixture +def power_sensor(gateway_nodes, power_sensor_state) -> Sensor: + """Load the power sensor.""" + nodes = update_gateway_nodes(gateway_nodes, power_sensor_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 69caeac9977..6edddc68592 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -1,6 +1,15 @@ """Provide tests for mysensors sensor platform.""" +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_POWER, + POWER_WATT, +) + + async def test_gps_sensor(hass, gps_sensor, integration): """Test a gps sensor.""" entity_id = "sensor.gps_sensor_1_1" @@ -8,3 +17,15 @@ async def test_gps_sensor(hass, gps_sensor, integration): state = hass.states.get(entity_id) assert state.state == "40.741894,-73.989311,12" + + +async def test_power_sensor(hass, power_sensor, integration): + """Test a power sensor.""" + entity_id = "sensor.power_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state.state == "1200" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT diff --git a/tests/fixtures/mysensors/power_sensor_state.json b/tests/fixtures/mysensors/power_sensor_state.json new file mode 100644 index 00000000000..40fcc4e4c74 --- /dev/null +++ b/tests/fixtures/mysensors/power_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 13, + "description": "", + "values": { + "17": "1200" + } + } + }, + "type": 17, + "sketch_name": "Power Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} From 736577abef9d5f608127e130957068a0ffa8b4bd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 19:51:07 +0200 Subject: [PATCH 650/818] Use EntityDescription - skybell (#53564) * Use EntityDescription - skybell * Fix typing --- .../components/skybell/binary_sensor.py | 68 ++++++++----------- homeassistant/components/skybell/sensor.py | 64 ++++++++--------- homeassistant/components/skybell/switch.py | 62 +++++++++-------- 3 files changed, 92 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index c6e8200c812..1e7eae145e3 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import NamedTuple +from typing import Any import voluptuous as vol @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, PLATFORM_SCHEMA, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -20,34 +21,27 @@ from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice SCAN_INTERVAL = timedelta(seconds=10) -class SkybellBinarySensorMetadata(NamedTuple): - """Metadata for an individual Skybell binary_sensor.""" - - name: str - device_class: str - event: str - - -SENSOR_TYPES = { - "button": SkybellBinarySensorMetadata( - "Button", +BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { + "button": BinarySensorEntityDescription( + key="device:sensor:button", + name="Button", device_class=DEVICE_CLASS_OCCUPANCY, - event="device:sensor:button", ), - "motion": SkybellBinarySensorMetadata( - "Motion", + "motion": BinarySensorEntityDescription( + key="device:sensor:motion", + name="Motion", device_class=DEVICE_CLASS_MOTION, - event="device:sensor:motion", ), } + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional( CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE ): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)] ), } ) @@ -57,36 +51,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the platform for a Skybell device.""" skybell = hass.data.get(SKYBELL_DOMAIN) - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - for device in skybell.get_devices(): - sensors.append(SkybellBinarySensor(device, sensor_type)) + binary_sensors = [ + SkybellBinarySensor(device, BINARY_SENSOR_TYPES[sensor_type]) + for device in skybell.get_devices() + for sensor_type in config[CONF_MONITORED_CONDITIONS] + ] - add_entities(sensors, True) + add_entities(binary_sensors, True) class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): """A binary sensor implementation for Skybell devices.""" - def __init__(self, device, sensor_type): + def __init__( + self, + device, + description: BinarySensorEntityDescription, + ): """Initialize a binary sensor for a Skybell device.""" super().__init__(device) - self._sensor_type = sensor_type - self._metadata = SENSOR_TYPES[self._sensor_type] - self._attr_name = f"{self._device.name} {self._metadata.name}" - self._device_class = self._metadata.device_class - self._event = {} - self._state = None - - @property - def is_on(self): - """Return True if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._device_class + self.entity_description = description + self._attr_name = f"{self._device.name} {description.name}" + self._event: dict[Any, Any] = {} @property def extra_state_attributes(self): @@ -101,8 +87,8 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): """Get the latest data and updates the state.""" super().update() - event = self._device.latest(self._metadata.event) + event = self._device.latest(self.entity_description.key) - self._state = bool(event and event.get("id") != self._event.get("id")) + self._attr_is_on = bool(event and event.get("id") != self._event.get("id")) self._event = event or {} diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index de99a22f4c9..cee864911b4 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -1,9 +1,15 @@ """Sensor support for Skybell Doorbells.""" +from __future__ import annotations + from datetime import timedelta import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -11,8 +17,15 @@ from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice SCAN_INTERVAL = timedelta(seconds=30) -# Sensor types: Name, icon -SENSOR_TYPES = {"chime_level": ["Chime Level", "bell-ring"]} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="chime_level", + name="Chime Level", + icon="mdi:bell-ring", + ), +) +MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -20,7 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE ): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] ), } ) @@ -30,10 +43,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the platform for a Skybell device.""" skybell = hass.data.get(SKYBELL_DOMAIN) - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - for device in skybell.get_devices(): - sensors.append(SkybellSensor(device, sensor_type)) + sensors = [ + SkybellSensor(device, description) + for device in skybell.get_devices() + for description in SENSOR_TYPES + if description.key in config[CONF_MONITORED_CONDITIONS] + ] add_entities(sensors, True) @@ -41,34 +56,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SkybellSensor(SkybellDevice, SensorEntity): """A sensor implementation for Skybell devices.""" - def __init__(self, device, sensor_type): + def __init__( + self, + device, + description: SensorEntityDescription, + ): """Initialize a sensor for a Skybell device.""" super().__init__(device) - self._sensor_type = sensor_type - self._icon = f"mdi:{SENSOR_TYPES[self._sensor_type][1]}" - self._name = "{} {}".format( - self._device.name, SENSOR_TYPES[self._sensor_type][0] - ) - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon + self.entity_description = description + self._attr_name = f"{self._device.name} {description.name}" def update(self): """Get the latest data and updates the state.""" super().update() - if self._sensor_type == "chime_level": - self._state = self._device.outdoor_chime_level + if self.entity_description.key == "chime_level": + self._attr_state = self._device.outdoor_chime_level diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index 5f9706de4d1..c842f0e91af 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -1,31 +1,30 @@ """Switch support for the Skybell HD Doorbell.""" from __future__ import annotations -from typing import NamedTuple - import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice - -class SkybellSwitchMetadata(NamedTuple): - """Metadata for an individual Skybell switch.""" - - name: str - - -SWITCH_TYPES = { - "do_not_disturb": SkybellSwitchMetadata( - "Do Not Disturb", +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key="do_not_disturb", + name="Do Not Disturb", ), - "motion_sensor": SkybellSwitchMetadata( - "Motion Sensor", + SwitchEntityDescription( + key="motion_sensor", + name="Motion Sensor", ), -} +) +MONITORED_CONDITIONS: list[str] = [desc.key for desc in SWITCH_TYPES] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE ): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SWITCH_TYPES)] + cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] ), } ) @@ -43,33 +42,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the platform for a Skybell device.""" skybell = hass.data.get(SKYBELL_DOMAIN) - sensors = [] - for switch_type in config.get(CONF_MONITORED_CONDITIONS): - for device in skybell.get_devices(): - sensors.append(SkybellSwitch(device, switch_type)) + switches = [ + SkybellSwitch(device, description) + for device in skybell.get_devices() + for description in SWITCH_TYPES + if description.key in config[CONF_MONITORED_CONDITIONS] + ] - add_entities(sensors, True) + add_entities(switches, True) class SkybellSwitch(SkybellDevice, SwitchEntity): """A switch implementation for Skybell devices.""" - def __init__(self, device, switch_type): + def __init__( + self, + device, + description: SwitchEntityDescription, + ): """Initialize a light for a Skybell device.""" super().__init__(device) - self._switch_type = switch_type - metadata = SWITCH_TYPES[self._switch_type] - self._attr_name = f"{self._device.name} {metadata.name}" + self.entity_description = description + self._attr_name = f"{self._device.name} {description.name}" def turn_on(self, **kwargs): """Turn on the switch.""" - setattr(self._device, self._switch_type, True) + setattr(self._device, self.entity_description.key, True) def turn_off(self, **kwargs): """Turn off the switch.""" - setattr(self._device, self._switch_type, False) + setattr(self._device, self.entity_description.key, False) @property def is_on(self): """Return true if device is on.""" - return getattr(self._device, self._switch_type) + return getattr(self._device, self.entity_description.key) From 13443310fea1e49a2dd834e27d4969369096b293 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 27 Jul 2021 14:03:13 -0400 Subject: [PATCH 651/818] Use entity class attributes for Cast (#53348) --- homeassistant/components/cast/media_player.py | 79 ++++++------------- 1 file changed, 22 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index fde9b23704d..74c90f43372 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -160,6 +160,9 @@ class CastDevice(MediaPlayerEntity): "elected leader" itself. """ + _attr_should_poll = False + _attr_media_image_remotely_accessible = True + def __init__(self, cast_info: ChromecastInfo) -> None: """Initialize the cast device.""" @@ -172,12 +175,21 @@ class CastDevice(MediaPlayerEntity): self.mz_media_status: dict[str, pychromecast.controllers.media.MediaStatus] = {} self.mz_media_status_received: dict[str, datetime] = {} self.mz_mgr = None - self._available = False + self._attr_available = False self._status_listener: CastStatusListener | None = None self._hass_cast_controller: HomeAssistantController | None = None self._add_remove_handler = None self._cast_view_remove_handler = None + self._attr_unique_id = cast_info.uuid + self._attr_name = cast_info.friendly_name + if cast_info.model_name != "Google Cast Group": + self._attr_device_info = { + "name": str(cast_info.friendly_name), + "identifiers": {(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, + "model": cast_info.model_name, + "manufacturer": str(cast_info.manufacturer), + } async def async_added_to_hass(self): """Create chromecast object when added to hass.""" @@ -239,7 +251,7 @@ class CastDevice(MediaPlayerEntity): self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr) - self._available = False + self._attr_available = False self.cast_status = chromecast.status self.media_status = chromecast.media_controller.status self._chromecast.start() @@ -255,7 +267,7 @@ class CastDevice(MediaPlayerEntity): self.entity_id, self._cast_info.friendly_name, ) - self._available = False + self._attr_available = False self.async_write_ha_state() await self.hass.async_add_executor_job(self._chromecast.disconnect) @@ -282,6 +294,10 @@ class CastDevice(MediaPlayerEntity): def new_cast_status(self, cast_status): """Handle updates of the cast status.""" self.cast_status = cast_status + self._attr_volume_level = cast_status.volume_level if cast_status else None + self._attr_is_volume_muted = ( + cast_status.volume_muted if self.cast_status else None + ) self.schedule_update_ha_state() def new_media_status(self, media_status): @@ -334,13 +350,13 @@ class CastDevice(MediaPlayerEntity): connection_status.status, ) if connection_status.status == CONNECTION_STATUS_DISCONNECTED: - self._available = False + self._attr_available = False self._invalidate() self.schedule_update_ha_state() return new_available = connection_status.status == CONNECTION_STATUS_CONNECTED - if new_available != self._available: + if new_available != self.available: # Connection status callbacks happen often when disconnected. # Only update state when availability changed to put less pressure # on state machine. @@ -350,7 +366,7 @@ class CastDevice(MediaPlayerEntity): self._cast_info.friendly_name, connection_status.status, ) - self._available = new_available + self._attr_available = new_available self.schedule_update_ha_state() def multizone_new_media_status(self, group_uuid, media_status): @@ -527,32 +543,6 @@ class CastDevice(MediaPlayerEntity): media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {}) ) - # ========== Properties ========== - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._cast_info.friendly_name - - @property - def device_info(self): - """Return information about the device.""" - cast_info = self._cast_info - - if cast_info.model_name == "Google Cast Group": - return None - - return { - "name": cast_info.friendly_name, - "identifiers": {(CAST_DOMAIN, cast_info.uuid.replace("-", ""))}, - "model": cast_info.model_name, - "manufacturer": cast_info.manufacturer, - } - def _media_status(self): """ Return media status. @@ -589,21 +579,6 @@ class CastDevice(MediaPlayerEntity): return STATE_OFF return None - @property - def available(self): - """Return True if the cast device is connected.""" - return self._available - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self.cast_status.volume_level if self.cast_status else None - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self.cast_status.volume_muted if self.cast_status else None - @property def media_content_id(self): """Content ID of current playing media.""" @@ -641,11 +616,6 @@ class CastDevice(MediaPlayerEntity): return images[0].url if images and images[0].url else None - @property - def media_image_remotely_accessible(self) -> bool: - """If the image url is remotely accessible.""" - return True - @property def media_title(self): """Title of current playing media.""" @@ -748,11 +718,6 @@ class CastDevice(MediaPlayerEntity): media_status_recevied = self._media_status()[1] return media_status_recevied - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._cast_info.uuid - async def _async_cast_discovered(self, discover: ChromecastInfo): """Handle discovery of new Chromecast.""" if self._cast_info.uuid != discover.uuid: From f599c5a39ebf65b3403219a7fd3081b334de9c6e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 20:13:48 +0200 Subject: [PATCH 652/818] Use EntityDescription - tibber (#53569) --- homeassistant/components/tibber/sensor.py | 196 ++++++++++++---------- 1 file changed, 111 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index c48f4936200..3ee1a3749d1 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -2,11 +2,11 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from enum import Enum import logging from random import randrange -from typing import NamedTuple import aiohttp @@ -20,6 +20,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, @@ -58,140 +59,158 @@ class ResetType(Enum): NEVER = "never" -class TibberSensorMetadata(NamedTuple): - """Metadata for an individual Tibber sensor.""" +@dataclass +class TibberSensorEntityDescription(SensorEntityDescription): + """Describes Tibber sensor entity.""" - name: str - device_class: str - unit: str | None = None - state_class: str | None = None reset_type: ResetType | None = None -RT_SENSOR_MAP: dict[str, TibberSensorMetadata] = { - "averagePower": TibberSensorMetadata( - "average power", +RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { + "averagePower": TibberSensorEntityDescription( + key="averagePower", + name="average power", device_class=DEVICE_CLASS_POWER, - unit=POWER_WATT, + unit_of_measurement=POWER_WATT, ), - "power": TibberSensorMetadata( - "power", + "power": TibberSensorEntityDescription( + key="power", + name="power", device_class=DEVICE_CLASS_POWER, - unit=POWER_WATT, + unit_of_measurement=POWER_WATT, ), - "powerProduction": TibberSensorMetadata( - "power production", + "powerProduction": TibberSensorEntityDescription( + key="powerProduction", + name="power production", device_class=DEVICE_CLASS_POWER, - unit=POWER_WATT, + unit_of_measurement=POWER_WATT, ), - "minPower": TibberSensorMetadata( - "min power", + "minPower": TibberSensorEntityDescription( + key="minPower", + name="min power", device_class=DEVICE_CLASS_POWER, - unit=POWER_WATT, + unit_of_measurement=POWER_WATT, ), - "maxPower": TibberSensorMetadata( - "max power", + "maxPower": TibberSensorEntityDescription( + key="maxPower", + name="max power", device_class=DEVICE_CLASS_POWER, - unit=POWER_WATT, + unit_of_measurement=POWER_WATT, ), - "accumulatedConsumption": TibberSensorMetadata( - "accumulated consumption", + "accumulatedConsumption": TibberSensorEntityDescription( + key="accumulatedConsumption", + name="accumulated consumption", device_class=DEVICE_CLASS_ENERGY, - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "accumulatedConsumptionLastHour": TibberSensorMetadata( - "accumulated consumption current hour", + "accumulatedConsumptionLastHour": TibberSensorEntityDescription( + key="accumulatedConsumptionLastHour", + name="accumulated consumption current hour", device_class=DEVICE_CLASS_ENERGY, - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.HOURLY, ), - "accumulatedProduction": TibberSensorMetadata( - "accumulated production", + "accumulatedProduction": TibberSensorEntityDescription( + key="accumulatedProduction", + name="accumulated production", device_class=DEVICE_CLASS_ENERGY, - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "accumulatedProductionLastHour": TibberSensorMetadata( - "accumulated production current hour", + "accumulatedProductionLastHour": TibberSensorEntityDescription( + key="accumulatedProductionLastHour", + name="accumulated production current hour", device_class=DEVICE_CLASS_ENERGY, - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.HOURLY, ), - "lastMeterConsumption": TibberSensorMetadata( - "last meter consumption", + "lastMeterConsumption": TibberSensorEntityDescription( + key="lastMeterConsumption", + name="last meter consumption", device_class=DEVICE_CLASS_ENERGY, - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, ), - "lastMeterProduction": TibberSensorMetadata( - "last meter production", + "lastMeterProduction": TibberSensorEntityDescription( + key="lastMeterProduction", + name="last meter production", device_class=DEVICE_CLASS_ENERGY, - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase1": TibberSensorMetadata( - "voltage phase1", + "voltagePhase1": TibberSensorEntityDescription( + key="voltagePhase1", + name="voltage phase1", device_class=DEVICE_CLASS_VOLTAGE, - unit=ELECTRIC_POTENTIAL_VOLT, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase2": TibberSensorMetadata( - "voltage phase2", + "voltagePhase2": TibberSensorEntityDescription( + key="voltagePhase2", + name="voltage phase2", device_class=DEVICE_CLASS_VOLTAGE, - unit=ELECTRIC_POTENTIAL_VOLT, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase3": TibberSensorMetadata( - "voltage phase3", + "voltagePhase3": TibberSensorEntityDescription( + key="voltagePhase3", + name="voltage phase3", device_class=DEVICE_CLASS_VOLTAGE, - unit=ELECTRIC_POTENTIAL_VOLT, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL1": TibberSensorMetadata( - "current L1", + "currentL1": TibberSensorEntityDescription( + key="currentL1", + name="current L1", device_class=DEVICE_CLASS_CURRENT, - unit=ELECTRIC_CURRENT_AMPERE, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL2": TibberSensorMetadata( - "current L2", + "currentL2": TibberSensorEntityDescription( + key="currentL2", + name="current L2", device_class=DEVICE_CLASS_CURRENT, - unit=ELECTRIC_CURRENT_AMPERE, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL3": TibberSensorMetadata( - "current L3", + "currentL3": TibberSensorEntityDescription( + key="currentL3", + name="current L3", device_class=DEVICE_CLASS_CURRENT, - unit=ELECTRIC_CURRENT_AMPERE, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "signalStrength": TibberSensorMetadata( - "signal strength", + "signalStrength": TibberSensorEntityDescription( + key="signalStrength", + name="signal strength", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - unit=SIGNAL_STRENGTH_DECIBELS, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, ), - "accumulatedReward": TibberSensorMetadata( - "accumulated reward", + "accumulatedReward": TibberSensorEntityDescription( + key="accumulatedReward", + name="accumulated reward", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "accumulatedCost": TibberSensorMetadata( - "accumulated cost", + "accumulatedCost": TibberSensorEntityDescription( + key="accumulatedCost", + name="accumulated cost", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "powerFactor": TibberSensorMetadata( - "power factor", + "powerFactor": TibberSensorEntityDescription( + key="powerFactor", + name="power factor", device_class=DEVICE_CLASS_POWER_FACTOR, - unit=PERCENTAGE, + unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), } @@ -358,31 +377,33 @@ class TibberSensorRT(TibberSensor): """Representation of a Tibber sensor for real time consumption.""" _attr_should_poll = False + entity_description: TibberSensorEntityDescription - def __init__(self, tibber_home, metadata: TibberSensorMetadata, initial_state): + def __init__( + self, + tibber_home, + description: TibberSensorEntityDescription, + initial_state, + ): """Initialize the sensor.""" super().__init__(tibber_home) + self.entity_description = description self._model = "Tibber Pulse" self._device_name = f"{self._model} {self._home_name}" - self._metadata = metadata - self._attr_device_class = metadata.device_class - self._attr_name = f"{metadata.name} {self._home_name}" + self._attr_name = f"{description.name} {self._home_name}" self._attr_state = initial_state - self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{metadata.name}" + self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}" - if metadata.name in ("accumulated cost", "accumulated reward"): + if description.name in ("accumulated cost", "accumulated reward"): self._attr_unit_of_measurement = tibber_home.currency - else: - self._attr_unit_of_measurement = metadata.unit - self._attr_state_class = metadata.state_class - if metadata.reset_type == ResetType.NEVER: + if description.reset_type == ResetType.NEVER: self._attr_last_reset = dt_util.utc_from_timestamp(0) - elif metadata.reset_type == ResetType.DAILY: + elif description.reset_type == ResetType.DAILY: self._attr_last_reset = dt_util.as_utc( dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) ) - elif metadata.reset_type == ResetType.HOURLY: + elif description.reset_type == ResetType.HOURLY: self._attr_last_reset = dt_util.as_utc( dt_util.now().replace(minute=0, second=0, microsecond=0) ) @@ -407,11 +428,17 @@ class TibberSensorRT(TibberSensor): @callback def _set_state(self, state, timestamp): """Set sensor state.""" - if state < self._attr_state and self._metadata.reset_type == ResetType.DAILY: + if ( + state < self._attr_state + and self.entity_description.reset_type == ResetType.DAILY + ): self._attr_last_reset = dt_util.as_utc( timestamp.replace(hour=0, minute=0, second=0, microsecond=0) ) - if state < self._attr_state and self._metadata.reset_type == ResetType.HOURLY: + if ( + state < self._attr_state + and self.entity_description.reset_type == ResetType.HOURLY + ): self._attr_last_reset = dt_util.as_utc( timestamp.replace(minute=0, second=0, microsecond=0) ) @@ -457,10 +484,9 @@ class TibberRtDataHandler: timestamp, ) else: - sensor_meta = RT_SENSOR_MAP[sensor_type] entity = TibberSensorRT( self._tibber_home, - sensor_meta, + RT_SENSOR_MAP[sensor_type], state, ) new_entities.append(entity) From 54a3c1a217bed82291d5b7c69d953a2441bc23f7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 27 Jul 2021 14:18:58 -0400 Subject: [PATCH 653/818] Use entity class attributes for clementine (#53405) --- .../components/clementine/media_player.py | 112 ++++-------------- 1 file changed, 26 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 44ba5c3d600..2d7099e1f54 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -67,18 +67,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ClementineDevice(MediaPlayerEntity): """Representation of Clementine Player.""" + _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_supported_features = SUPPORT_CLEMENTINE + def __init__(self, client, name): """Initialize the Clementine device.""" self._client = client - self._name = name - self._muted = False - self._volume = 0.0 - self._track_id = 0 - self._last_track_id = 0 - self._track_name = "" - self._track_artist = "" - self._track_album_name = "" - self._state = None + self._attr_name = name def update(self): """Retrieve the latest data from the Clementine Player.""" @@ -86,59 +81,37 @@ class ClementineDevice(MediaPlayerEntity): client = self._client if client.state == "Playing": - self._state = STATE_PLAYING + self._attr_state = STATE_PLAYING elif client.state == "Paused": - self._state = STATE_PAUSED + self._attr_state = STATE_PAUSED elif client.state == "Disconnected": - self._state = STATE_OFF + self._attr_state = STATE_OFF else: - self._state = STATE_PAUSED + self._attr_state = STATE_PAUSED if client.last_update and (time.time() - client.last_update > 40): - self._state = STATE_OFF + self._attr_state = STATE_OFF - self._volume = float(client.volume) if client.volume else 0.0 + volume = float(client.volume) if client.volume else 0.0 + self._attr_volume_level = volume / 100.0 + if client.active_playlist_id in client.playlists: + self._attr_source = client.playlists[client.active_playlist_id]["name"] + else: + self._attr_source = "Unknown" + self._attr_source_list = [s["name"] for s in client.playlists.values()] if client.current_track: - self._track_id = client.current_track["track_id"] - self._track_name = client.current_track["title"] - self._track_artist = client.current_track["track_artist"] - self._track_album_name = client.current_track["track_album"] + self._attr_media_title = client.current_track["title"] + self._attr_media_artist = client.current_track["track_artist"] + self._attr_media_album_name = client.current_track["track_album"] + self._attr_media_image_hash = client.current_track["track_id"] + else: + self._attr_media_image_hash = None except Exception: - self._state = STATE_OFF + self._attr_state = STATE_OFF raise - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume / 100.0 - - @property - def source(self): - """Return current source name.""" - source_name = "Unknown" - client = self._client - if client.active_playlist_id in client.playlists: - source_name = client.playlists[client.active_playlist_id]["name"] - return source_name - - @property - def source_list(self): - """List of available input sources.""" - source_names = [s["name"] for s in self._client.playlists.values()] - return source_names - def select_source(self, source): """Select input source.""" client = self._client @@ -146,39 +119,6 @@ class ClementineDevice(MediaPlayerEntity): if len(sources) == 1: client.change_song(sources[0]["id"], 0) - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def media_title(self): - """Title of current playing media.""" - return self._track_name - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - return self._track_artist - - @property - def media_album_name(self): - """Album name of current playing media, music track only.""" - return self._track_album_name - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_CLEMENTINE - - @property - def media_image_hash(self): - """Hash value for media image.""" - if self._client.current_track: - return self._client.current_track["track_id"] - - return None - async def async_get_media_image(self): """Fetch media image of current playing image.""" if self._client.current_track: @@ -207,19 +147,19 @@ class ClementineDevice(MediaPlayerEntity): def media_play_pause(self): """Simulate play pause media player.""" - if self._state == STATE_PLAYING: + if self.state == STATE_PLAYING: self.media_pause() else: self.media_play() def media_play(self): """Send play command.""" - self._state = STATE_PLAYING + self._attr_state = STATE_PLAYING self._client.play() def media_pause(self): """Send media pause command to media player.""" - self._state = STATE_PAUSED + self._attr_state = STATE_PAUSED self._client.pause() def media_next_track(self): From 37c841956f4d1108fb50b53df7e904633ac9dcaa Mon Sep 17 00:00:00 2001 From: Matthew Gottlieb <87102888+matthewgottlieb@users.noreply.github.com> Date: Tue, 27 Jul 2021 14:28:04 -0400 Subject: [PATCH 654/818] Allow removing workday holidays by name (#52700) --- .../components/workday/binary_sensor.py | 15 +++++++++++- .../components/workday/test_binary_sensor.py | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 533bc77e27c..fc726d56f04 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -103,7 +103,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Remove holidays try: for date in remove_holidays: - obj_holidays.pop(date) + try: + # is this formatted as a date? + if dt.parse_date(date): + # remove holiday by date + removed = obj_holidays.pop(date) + _LOGGER.debug("Removed %s", date) + else: + # remove holiday by name + _LOGGER.debug("Treating '%s' as named holiday", date) + removed = obj_holidays.pop_named(date) + for holiday in removed: + _LOGGER.debug("Removed %s by name '%s'", holiday, date) + except KeyError as unmatched: + _LOGGER.warning("No holiday found matching %s", unmatched) except TypeError: _LOGGER.debug("No holidays to remove or invalid holidays") diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 3ec17c3e6d3..f8ab8794c0d 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -85,6 +85,16 @@ class TestWorkdaySetup: } } + self.config_remove_named_holidays = { + "binary_sensor": { + "platform": "workday", + "country": "US", + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "excludes": ["sat", "sun", "holiday"], + "remove_holidays": ["Not a Holiday", "Christmas", "Thanksgiving"], + } + } + self.config_tomorrow = { "binary_sensor": {"platform": "workday", "country": "DE", "days_offset": 1} } @@ -320,3 +330,17 @@ class TestWorkdaySetup: entity = self.hass.states.get("binary_sensor.workday_sensor") assert entity.state == "on" + + # Freeze time to test Fri, but remove holiday by name - Christmas + @patch(FUNCTION_PATH, return_value=date(2020, 12, 25)) + def test_config_remove_named_holidays_xmas(self, mock_date): + """Test if removed by name holidays are reported correctly.""" + with assert_setup_component(1, "binary_sensor"): + setup_component( + self.hass, "binary_sensor", self.config_remove_named_holidays + ) + + self.hass.start() + + entity = self.hass.states.get("binary_sensor.workday_sensor") + assert entity.state == "on" From 43d3b6c2a2ca73712a48ffa2710f4db1dc6d9c2b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 27 Jul 2021 15:53:29 -0300 Subject: [PATCH 655/818] Move the coordinator logic to the BroadlinkEntity class (#53571) --- homeassistant/components/broadlink/entity.py | 32 +++++++++++++++- homeassistant/components/broadlink/remote.py | 10 +---- homeassistant/components/broadlink/sensor.py | 19 ++-------- homeassistant/components/broadlink/switch.py | 40 +++++--------------- 4 files changed, 45 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index fc5d22302a6..bd2f938a2bd 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -12,8 +12,38 @@ class BroadlinkEntity(Entity): _attr_should_poll = False def __init__(self, device): - """Initialize the device.""" + """Initialize the entity.""" self._device = device + self._coordinator = device.update_manager.coordinator + + async def async_added_to_hass(self): + """Call when the entity is added to hass.""" + self.async_on_remove(self._coordinator.async_add_listener(self._recv_data)) + + async def async_update(self): + """Update the state of the entity.""" + await self._coordinator.async_request_refresh() + + def _recv_data(self): + """Receive data from the update coordinator. + + This event listener should be called by the coordinator whenever + there is an update available. + + It works as a template for the _update_state() method, which should + be overridden by child classes in order to update the state of the + entities, when applicable. + """ + if self._coordinator.last_update_success: + self._update_state(self._coordinator.data) + self.async_write_ha_state() + + def _update_state(self, data): + """Update the state of the entity. + + This method should be overridden by child classes in order to + internalize state and attributes received from the coordinator. + """ @property def available(self): diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 3bb85ab9d85..a0c5c4130e5 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -119,7 +119,6 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): def __init__(self, device, codes, flags): """Initialize the remote.""" super().__init__(device) - self._coordinator = device.update_manager.coordinator self._code_storage = codes self._flag_storage = flags self._storage_loaded = False @@ -189,14 +188,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """Call when the remote is added to hass.""" state = await self.async_get_last_state() self._attr_is_on = state is None or state.state != STATE_OFF - - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the remote.""" - await self._coordinator.async_request_refresh() + await super().async_added_to_hass() async def async_turn_on(self, **kwargs): """Turn on the remote.""" diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 2e5a82e1217..f73f669326d 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import CONF_HOST, PERCENTAGE, POWER_WATT, TEMP_CELSIUS -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -81,7 +80,6 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity): def __init__(self, device, monitored_condition): """Initialize the sensor.""" super().__init__(device) - self._coordinator = device.update_manager.coordinator self._monitored_condition = monitored_condition self._attr_device_class = SENSOR_TYPES[monitored_condition][2] @@ -91,17 +89,6 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity): self._attr_unique_id = f"{device.unique_id}-{monitored_condition}" self._attr_unit_of_measurement = SENSOR_TYPES[monitored_condition][1] - @callback - def update_data(self): - """Update data.""" - if self._coordinator.last_update_success: - self._attr_state = self._coordinator.data[self._monitored_condition] - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Call when the sensor is added to hass.""" - self.async_on_remove(self._coordinator.async_add_listener(self.update_data)) - - async def async_update(self): - """Update the sensor.""" - await self._coordinator.async_request_refresh() + def _update_state(self, data): + """Update the state of the entity.""" + self._attr_state = data[self._monitored_condition] diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 3333c370824..0f380b6cca2 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -24,7 +24,6 @@ from homeassistant.const import ( CONF_TYPE, STATE_ON, ) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -143,27 +142,17 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): super().__init__(device) self._command_on = command_on self._command_off = command_off - self._coordinator = device.update_manager.coordinator self._attr_assumed_state = True self._attr_device_class = DEVICE_CLASS_SWITCH self._attr_name = f"{device.name} Switch" self._attr_unique_id = device.unique_id - @callback - def update_data(self): - """Update data.""" - self.async_write_ha_state() - async def async_added_to_hass(self): """Call when the switch is added to hass.""" state = await self.async_get_last_state() self._attr_is_on = state is not None and state.state == STATE_ON - self.async_on_remove(self._coordinator.async_add_listener(self.update_data)) - - async def async_update(self): - """Update the switch.""" - await self._coordinator.async_request_refresh() + await super().async_added_to_hass() async def async_turn_on(self, **kwargs): """Turn on the switch.""" @@ -233,12 +222,9 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): super().__init__(device, *args, **kwargs) self._attr_is_on = self._coordinator.data["pwr"] - @callback - def update_data(self): - """Update data.""" - if self._coordinator.last_update_success: - self._attr_is_on = self._coordinator.data["pwr"] - self.async_write_ha_state() + def _update_state(self, data): + """Update the state of the entity.""" + self._attr_is_on = data["pwr"] class BroadlinkMP1Slot(BroadlinkSwitch): @@ -254,12 +240,9 @@ class BroadlinkMP1Slot(BroadlinkSwitch): self._attr_name = f"{device.name} S{slot}" self._attr_unique_id = f"{device.unique_id}-s{slot}" - @callback - def update_data(self): - """Update data.""" - if self._coordinator.last_update_success: - self._attr_is_on = self._coordinator.data[f"s{self._slot}"] - self.async_write_ha_state() + def _update_state(self, data): + """Update the state of the entity.""" + self._attr_is_on = data[f"s{self._slot}"] async def _async_send_packet(self, packet): """Send a packet to the device.""" @@ -288,12 +271,9 @@ class BroadlinkBG1Slot(BroadlinkSwitch): self._attr_device_class = DEVICE_CLASS_OUTLET self._attr_unique_id = f"{device.unique_id}-s{slot}" - @callback - def update_data(self): - """Update data.""" - if self._coordinator.last_update_success: - self._attr_is_on = self._coordinator.data[f"pwr{self._slot}"] - self.async_write_ha_state() + def _update_state(self, data): + """Update the state of the entity.""" + self._attr_is_on = data[f"pwr{self._slot}"] async def _async_send_packet(self, packet): """Send a packet to the device.""" From f3bf0fdb09319d1044c6bb74f0413331cd3d7daa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Jul 2021 14:17:17 -0500 Subject: [PATCH 656/818] Bump yalexs to 1.1.13 to fix august doorsense offline at startup (#53574) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 9f8b435b714..74caa4b4a78 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.12"], + "requirements": ["yalexs==1.1.13"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 38d7bd57c7c..315907c4078 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2414,7 +2414,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.12 +yalexs==1.1.13 # homeassistant.components.yeelight yeelight==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36de2c46daf..6413f95cf28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1329,7 +1329,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.12 +yalexs==1.1.13 # homeassistant.components.yeelight yeelight==0.6.3 From 5a6be2370b447d2e81b8289b8ef0ebcdda593688 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 27 Jul 2021 21:19:29 +0200 Subject: [PATCH 657/818] Replace util.get_local_ip in favor of components.network.async_get_source_ip() - part 3 (#53424) --- homeassistant/components/emulated_roku/__init__.py | 8 ++++++-- homeassistant/components/emulated_roku/manifest.json | 1 + tests/components/emulated_roku/test_init.py | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 4e577929644..45c9355603f 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -1,7 +1,9 @@ """Support for Roku API emulation.""" import voluptuous as vol -from homeassistant import config_entries, util +from homeassistant import config_entries +from homeassistant.components.network import async_get_source_ip +from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -71,7 +73,9 @@ async def async_setup_entry(hass, config_entry): name = config[CONF_NAME] listen_port = config[CONF_LISTEN_PORT] - host_ip = config.get(CONF_HOST_IP) or util.get_local_ip() + host_ip = config.get(CONF_HOST_IP) or await async_get_source_ip( + hass, PUBLIC_TARGET_IP + ) advertise_ip = config.get(CONF_ADVERTISE_IP) advertise_port = config.get(CONF_ADVERTISE_PORT) upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST) diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 6ef54d1d1cc..36a86137e87 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "requirements": ["emulated_roku==0.2.1"], + "dependencies": ["network"], "codeowners": [], "iot_class": "local_push" } diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 8f256ee4c79..d69df5a1fbe 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -93,7 +93,11 @@ async def test_setup_entry_successful(hass): async def test_unload_entry(hass): """Test being able to unload an entry.""" entry = Mock() - entry.data = {"name": "Emulated Roku Test", "listen_port": 8060} + entry.data = { + "name": "Emulated Roku Test", + "listen_port": 8060, + emulated_roku.CONF_HOST_IP: "1.2.3.5", + } with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", From e04b2c2e350e62a29346c0386779b502866c18bc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 27 Jul 2021 13:39:58 -0600 Subject: [PATCH 658/818] Bump pyairvisual to 5.0.9 (#53578) --- homeassistant/components/airvisual/__init__.py | 8 +++----- homeassistant/components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 21d4054f6ee..c44e39b59e4 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta from math import ceil -from typing import Any, Dict, cast +from typing import Any from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -216,8 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) try: - data = await api_coro - return cast(Dict[str, Any], data) + return await api_coro except (InvalidKeyError, KeyExpiredError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: @@ -261,8 +260,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async with NodeSamba( config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] ) as node: - data = await node.async_get_latest_measurements() - return cast(Dict[str, Any], data) + return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index b94218f6c13..5d6a221dbbe 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==5.0.8"], + "requirements": ["pyairvisual==5.0.9"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 315907c4078..d3b375a53f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1311,7 +1311,7 @@ pyaftership==0.1.2 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.8 +pyairvisual==5.0.9 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6413f95cf28..7f0dca47aac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.8 +pyairvisual==5.0.9 # homeassistant.components.almond pyalmond==0.0.2 From 348805364843529507bd8ac08d227c704fb5d57f Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 27 Jul 2021 21:49:49 +0200 Subject: [PATCH 659/818] Remove garmin_connect integration (#52808) Co-authored-by: Franck Nijhof --- .coveragerc | 4 - CODEOWNERS | 1 - .../components/garmin_connect/__init__.py | 107 ------ .../components/garmin_connect/alarm_util.py | 50 --- .../components/garmin_connect/config_flow.py | 72 ---- .../components/garmin_connect/const.py | 355 ------------------ .../components/garmin_connect/manifest.json | 9 - .../components/garmin_connect/sensor.py | 199 ---------- .../components/garmin_connect/strings.json | 23 -- .../garmin_connect/translations/ca.json | 23 -- .../garmin_connect/translations/cs.json | 23 -- .../garmin_connect/translations/da.json | 23 -- .../garmin_connect/translations/de.json | 23 -- .../garmin_connect/translations/en.json | 23 -- .../garmin_connect/translations/es-419.json | 23 -- .../garmin_connect/translations/es.json | 23 -- .../garmin_connect/translations/et.json | 23 -- .../garmin_connect/translations/fr.json | 23 -- .../garmin_connect/translations/he.json | 21 -- .../garmin_connect/translations/hu.json | 23 -- .../garmin_connect/translations/id.json | 23 -- .../garmin_connect/translations/it.json | 23 -- .../garmin_connect/translations/ko.json | 23 -- .../garmin_connect/translations/lb.json | 23 -- .../garmin_connect/translations/lv.json | 12 - .../garmin_connect/translations/nl.json | 23 -- .../garmin_connect/translations/no.json | 23 -- .../garmin_connect/translations/pl.json | 23 -- .../garmin_connect/translations/pt-BR.json | 10 - .../garmin_connect/translations/pt.json | 22 -- .../garmin_connect/translations/ru.json | 23 -- .../garmin_connect/translations/sl.json | 23 -- .../garmin_connect/translations/sv.json | 23 -- .../garmin_connect/translations/tr.json | 20 - .../garmin_connect/translations/uk.json | 23 -- .../garmin_connect/translations/zh-Hans.json | 11 - .../garmin_connect/translations/zh-Hant.json | 23 -- homeassistant/generated/config_flows.py | 1 - mypy.ini | 3 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - script/hassfest/mypy_config.py | 1 - tests/components/garmin_connect/__init__.py | 1 - .../garmin_connect/test_config_flow.py | 112 ------ 44 files changed, 1546 deletions(-) delete mode 100644 homeassistant/components/garmin_connect/__init__.py delete mode 100644 homeassistant/components/garmin_connect/alarm_util.py delete mode 100644 homeassistant/components/garmin_connect/config_flow.py delete mode 100644 homeassistant/components/garmin_connect/const.py delete mode 100644 homeassistant/components/garmin_connect/manifest.json delete mode 100644 homeassistant/components/garmin_connect/sensor.py delete mode 100644 homeassistant/components/garmin_connect/strings.json delete mode 100644 homeassistant/components/garmin_connect/translations/ca.json delete mode 100644 homeassistant/components/garmin_connect/translations/cs.json delete mode 100644 homeassistant/components/garmin_connect/translations/da.json delete mode 100644 homeassistant/components/garmin_connect/translations/de.json delete mode 100644 homeassistant/components/garmin_connect/translations/en.json delete mode 100644 homeassistant/components/garmin_connect/translations/es-419.json delete mode 100644 homeassistant/components/garmin_connect/translations/es.json delete mode 100644 homeassistant/components/garmin_connect/translations/et.json delete mode 100644 homeassistant/components/garmin_connect/translations/fr.json delete mode 100644 homeassistant/components/garmin_connect/translations/he.json delete mode 100644 homeassistant/components/garmin_connect/translations/hu.json delete mode 100644 homeassistant/components/garmin_connect/translations/id.json delete mode 100644 homeassistant/components/garmin_connect/translations/it.json delete mode 100644 homeassistant/components/garmin_connect/translations/ko.json delete mode 100644 homeassistant/components/garmin_connect/translations/lb.json delete mode 100644 homeassistant/components/garmin_connect/translations/lv.json delete mode 100644 homeassistant/components/garmin_connect/translations/nl.json delete mode 100644 homeassistant/components/garmin_connect/translations/no.json delete mode 100644 homeassistant/components/garmin_connect/translations/pl.json delete mode 100644 homeassistant/components/garmin_connect/translations/pt-BR.json delete mode 100644 homeassistant/components/garmin_connect/translations/pt.json delete mode 100644 homeassistant/components/garmin_connect/translations/ru.json delete mode 100644 homeassistant/components/garmin_connect/translations/sl.json delete mode 100644 homeassistant/components/garmin_connect/translations/sv.json delete mode 100644 homeassistant/components/garmin_connect/translations/tr.json delete mode 100644 homeassistant/components/garmin_connect/translations/uk.json delete mode 100644 homeassistant/components/garmin_connect/translations/zh-Hans.json delete mode 100644 homeassistant/components/garmin_connect/translations/zh-Hant.json delete mode 100644 tests/components/garmin_connect/__init__.py delete mode 100644 tests/components/garmin_connect/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 22e81dc57c8..30bfb697b41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -361,10 +361,6 @@ omit = homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/sensor.py - homeassistant/components/garmin_connect/__init__.py - homeassistant/components/garmin_connect/const.py - homeassistant/components/garmin_connect/sensor.py - homeassistant/components/garmin_connect/alarm_util.py homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/generic_hygrostat/* diff --git a/CODEOWNERS b/CODEOWNERS index 9ce94438a30..9b18f1a1b23 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -179,7 +179,6 @@ homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garages_amsterdam/* @klaasnicolaas -homeassistant/components/garmin_connect/* @cyberjunky homeassistant/components/gdacs/* @exxamalte homeassistant/components/generic_hygrostat/* @Shulyaka homeassistant/components/geniushub/* @zxdavb diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py deleted file mode 100644 index 180fcdb08a2..00000000000 --- a/homeassistant/components/garmin_connect/__init__.py +++ /dev/null @@ -1,107 +0,0 @@ -"""The Garmin Connect integration.""" -from datetime import date -import logging - -from garminconnect_ha import ( - Garmin, - GarminConnectAuthenticationError, - GarminConnectConnectionError, - GarminConnectTooManyRequestsError, -) - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.util import Throttle - -from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -PLATFORMS = ["sensor"] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Garmin Connect from a config entry.""" - - username: str = entry.data[CONF_USERNAME] - password: str = entry.data[CONF_PASSWORD] - - api = Garmin(username, password) - - try: - await hass.async_add_executor_job(api.login) - except ( - GarminConnectAuthenticationError, - GarminConnectTooManyRequestsError, - ) as err: - _LOGGER.error("Error occurred during Garmin Connect login request: %s", err) - return False - except (GarminConnectConnectionError) as err: - _LOGGER.error( - "Connection error occurred during Garmin Connect login request: %s", err - ) - raise ConfigEntryNotReady from err - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error occurred during Garmin Connect login request") - return False - - garmin_data = GarminConnectData(hass, api) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = garmin_data - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok - - -class GarminConnectData: - """Define an object to hold sensor data.""" - - def __init__(self, hass, client): - """Initialize.""" - self.hass = hass - self.client = client - self.data = None - - @Throttle(DEFAULT_UPDATE_INTERVAL) - async def async_update(self): - """Update data via API wrapper.""" - today = date.today() - - try: - summary = await self.hass.async_add_executor_job( - self.client.get_user_summary, today.isoformat() - ) - body = await self.hass.async_add_executor_job( - self.client.get_body_composition, today.isoformat() - ) - - self.data = { - **summary, - **body["totalAverage"], - } - self.data["nextAlarm"] = await self.hass.async_add_executor_job( - self.client.get_device_alarms - ) - except ( - GarminConnectAuthenticationError, - GarminConnectTooManyRequestsError, - GarminConnectConnectionError, - ) as err: - _LOGGER.error( - "Error occurred during Garmin Connect update requests: %s", err - ) - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Unknown error occurred during Garmin Connect update requests" - ) diff --git a/homeassistant/components/garmin_connect/alarm_util.py b/homeassistant/components/garmin_connect/alarm_util.py deleted file mode 100644 index 4964d70e886..00000000000 --- a/homeassistant/components/garmin_connect/alarm_util.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Utility method for converting Garmin Connect alarms to python datetime.""" -from datetime import date, datetime, timedelta -import logging - -_LOGGER = logging.getLogger(__name__) - -DAY_TO_NUMBER = { - "Mo": 1, - "M": 1, - "Tu": 2, - "We": 3, - "W": 3, - "Th": 4, - "Fr": 5, - "F": 5, - "Sa": 6, - "Su": 7, -} - - -def calculate_next_active_alarms(alarms): - """ - Calculate garmin next active alarms from settings. - - Alarms are sorted by time - """ - active_alarms = [] - _LOGGER.debug(alarms) - - for alarm_setting in alarms: - if alarm_setting["alarmMode"] != "ON": - continue - for day in alarm_setting["alarmDays"]: - alarm_time = alarm_setting["alarmTime"] - if day == "ONCE": - midnight = datetime.combine(date.today(), datetime.min.time()) - alarm = midnight + timedelta(minutes=alarm_time) - if alarm < datetime.now(): - alarm += timedelta(days=1) - else: - start_of_week = datetime.combine( - date.today() - timedelta(days=datetime.today().isoweekday() % 7), - datetime.min.time(), - ) - days_to_add = DAY_TO_NUMBER[day] % 7 - alarm = start_of_week + timedelta(minutes=alarm_time, days=days_to_add) - if alarm < datetime.now(): - alarm += timedelta(days=7) - active_alarms.append(alarm.isoformat()) - return sorted(active_alarms) if active_alarms else None diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py deleted file mode 100644 index e9966859f99..00000000000 --- a/homeassistant/components/garmin_connect/config_flow.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Config flow for Garmin Connect integration.""" -import logging - -from garminconnect_ha import ( - Garmin, - GarminConnectAuthenticationError, - GarminConnectConnectionError, - GarminConnectTooManyRequestsError, -) -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Garmin Connect.""" - - VERSION = 1 - - async def _show_setup_form(self, errors=None): - """Show the setup form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} - ), - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - if user_input is None: - return await self._show_setup_form() - - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - - api = Garmin(username, password) - - errors = {} - try: - await self.hass.async_add_executor_job(api.login) - except GarminConnectConnectionError: - errors["base"] = "cannot_connect" - return await self._show_setup_form(errors) - except GarminConnectAuthenticationError: - errors["base"] = "invalid_auth" - return await self._show_setup_form(errors) - except GarminConnectTooManyRequestsError: - errors["base"] = "too_many_requests" - return await self._show_setup_form(errors) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - return await self._show_setup_form(errors) - - await self.async_set_unique_id(username) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=username, - data={ - CONF_ID: username, - CONF_USERNAME: username, - CONF_PASSWORD: password, - }, - ) diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py deleted file mode 100644 index 19ed4ca4d94..00000000000 --- a/homeassistant/components/garmin_connect/const.py +++ /dev/null @@ -1,355 +0,0 @@ -"""Constants for the Garmin Connect integration.""" -from datetime import timedelta - -from homeassistant.const import ( - DEVICE_CLASS_TIMESTAMP, - LENGTH_METERS, - MASS_KILOGRAMS, - PERCENTAGE, - TIME_MINUTES, -) - -DOMAIN = "garmin_connect" -ATTRIBUTION = "connect.garmin.com" -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) - -GARMIN_ENTITY_LIST = { - "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], - "dailyStepGoal": ["Daily Step Goal", "steps", "mdi:walk", None, True], - "totalKilocalories": ["Total KiloCalories", "kcal", "mdi:food", None, True], - "activeKilocalories": ["Active KiloCalories", "kcal", "mdi:food", None, True], - "bmrKilocalories": ["BMR KiloCalories", "kcal", "mdi:food", None, True], - "consumedKilocalories": ["Consumed KiloCalories", "kcal", "mdi:food", None, False], - "burnedKilocalories": ["Burned KiloCalories", "kcal", "mdi:food", None, True], - "remainingKilocalories": [ - "Remaining KiloCalories", - "kcal", - "mdi:food", - None, - False, - ], - "netRemainingKilocalories": [ - "Net Remaining KiloCalories", - "kcal", - "mdi:food", - None, - False, - ], - "netCalorieGoal": ["Net Calorie Goal", "cal", "mdi:food", None, False], - "totalDistanceMeters": [ - "Total Distance Mtr", - LENGTH_METERS, - "mdi:walk", - None, - True, - ], - "wellnessStartTimeLocal": [ - "Wellness Start Time", - None, - "mdi:clock", - DEVICE_CLASS_TIMESTAMP, - False, - ], - "wellnessEndTimeLocal": [ - "Wellness End Time", - None, - "mdi:clock", - DEVICE_CLASS_TIMESTAMP, - False, - ], - "wellnessDescription": ["Wellness Description", "", "mdi:clock", None, False], - "wellnessDistanceMeters": [ - "Wellness Distance Mtr", - LENGTH_METERS, - "mdi:walk", - None, - False, - ], - "wellnessActiveKilocalories": [ - "Wellness Active KiloCalories", - "kcal", - "mdi:food", - None, - False, - ], - "wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food", None, False], - "highlyActiveSeconds": [ - "Highly Active Time", - TIME_MINUTES, - "mdi:fire", - None, - False, - ], - "activeSeconds": ["Active Time", TIME_MINUTES, "mdi:fire", None, True], - "sedentarySeconds": ["Sedentary Time", TIME_MINUTES, "mdi:seat", None, True], - "sleepingSeconds": ["Sleeping Time", TIME_MINUTES, "mdi:sleep", None, True], - "measurableAwakeDuration": [ - "Awake Duration", - TIME_MINUTES, - "mdi:sleep", - None, - True, - ], - "measurableAsleepDuration": [ - "Sleep Duration", - TIME_MINUTES, - "mdi:sleep", - None, - True, - ], - "floorsAscendedInMeters": [ - "Floors Ascended Mtr", - LENGTH_METERS, - "mdi:stairs", - None, - False, - ], - "floorsDescendedInMeters": [ - "Floors Descended Mtr", - LENGTH_METERS, - "mdi:stairs", - None, - False, - ], - "floorsAscended": ["Floors Ascended", "floors", "mdi:stairs", None, True], - "floorsDescended": ["Floors Descended", "floors", "mdi:stairs", None, True], - "userFloorsAscendedGoal": [ - "Floors Ascended Goal", - "floors", - "mdi:stairs", - None, - True, - ], - "minHeartRate": ["Min Heart Rate", "bpm", "mdi:heart-pulse", None, True], - "maxHeartRate": ["Max Heart Rate", "bpm", "mdi:heart-pulse", None, True], - "restingHeartRate": ["Resting Heart Rate", "bpm", "mdi:heart-pulse", None, True], - "minAvgHeartRate": ["Min Avg Heart Rate", "bpm", "mdi:heart-pulse", None, False], - "maxAvgHeartRate": ["Max Avg Heart Rate", "bpm", "mdi:heart-pulse", None, False], - "abnormalHeartRateAlertsCount": [ - "Abnormal HR Counts", - "", - "mdi:heart-pulse", - None, - False, - ], - "lastSevenDaysAvgRestingHeartRate": [ - "Last 7 Days Avg Heart Rate", - "bpm", - "mdi:heart-pulse", - None, - False, - ], - "averageStressLevel": ["Avg Stress Level", "", "mdi:flash-alert", None, True], - "maxStressLevel": ["Max Stress Level", "", "mdi:flash-alert", None, True], - "stressQualifier": ["Stress Qualifier", "", "mdi:flash-alert", None, False], - "stressDuration": ["Stress Duration", TIME_MINUTES, "mdi:flash-alert", None, False], - "restStressDuration": [ - "Rest Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "activityStressDuration": [ - "Activity Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "uncategorizedStressDuration": [ - "Uncat. Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "totalStressDuration": [ - "Total Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "lowStressDuration": [ - "Low Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "mediumStressDuration": [ - "Medium Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "highStressDuration": [ - "High Stress Duration", - TIME_MINUTES, - "mdi:flash-alert", - None, - True, - ], - "stressPercentage": [ - "Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "restStressPercentage": [ - "Rest Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "activityStressPercentage": [ - "Activity Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "uncategorizedStressPercentage": [ - "Uncat. Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "lowStressPercentage": [ - "Low Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "mediumStressPercentage": [ - "Medium Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "highStressPercentage": [ - "High Stress Percentage", - PERCENTAGE, - "mdi:flash-alert", - None, - False, - ], - "moderateIntensityMinutes": [ - "Moderate Intensity", - TIME_MINUTES, - "mdi:flash-alert", - None, - False, - ], - "vigorousIntensityMinutes": [ - "Vigorous Intensity", - TIME_MINUTES, - "mdi:run-fast", - None, - False, - ], - "intensityMinutesGoal": [ - "Intensity Goal", - TIME_MINUTES, - "mdi:run-fast", - None, - False, - ], - "bodyBatteryChargedValue": [ - "Body Battery Charged", - PERCENTAGE, - "mdi:battery-charging-100", - None, - True, - ], - "bodyBatteryDrainedValue": [ - "Body Battery Drained", - PERCENTAGE, - "mdi:battery-alert-variant-outline", - None, - True, - ], - "bodyBatteryHighestValue": [ - "Body Battery Highest", - PERCENTAGE, - "mdi:battery-heart", - None, - True, - ], - "bodyBatteryLowestValue": [ - "Body Battery Lowest", - PERCENTAGE, - "mdi:battery-heart-outline", - None, - True, - ], - "bodyBatteryMostRecentValue": [ - "Body Battery Most Recent", - PERCENTAGE, - "mdi:battery-positive", - None, - True, - ], - "averageSpo2": ["Average SPO2", PERCENTAGE, "mdi:diabetes", None, True], - "lowestSpo2": ["Lowest SPO2", PERCENTAGE, "mdi:diabetes", None, True], - "latestSpo2": ["Latest SPO2", PERCENTAGE, "mdi:diabetes", None, True], - "latestSpo2ReadingTimeLocal": [ - "Latest SPO2 Time", - None, - "mdi:diabetes", - DEVICE_CLASS_TIMESTAMP, - False, - ], - "averageMonitoringEnvironmentAltitude": [ - "Average Altitude", - PERCENTAGE, - "mdi:image-filter-hdr", - None, - False, - ], - "highestRespirationValue": [ - "Highest Respiration", - "brpm", - "mdi:progress-clock", - None, - False, - ], - "lowestRespirationValue": [ - "Lowest Respiration", - "brpm", - "mdi:progress-clock", - None, - False, - ], - "latestRespirationValue": [ - "Latest Respiration", - "brpm", - "mdi:progress-clock", - None, - False, - ], - "latestRespirationTimeGMT": [ - "Latest Respiration Update", - None, - "mdi:progress-clock", - DEVICE_CLASS_TIMESTAMP, - False, - ], - "weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", None, False], - "bmi": ["BMI", "", "mdi:food", None, False], - "bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, False], - "bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", None, False], - "bodyMass": ["Body Mass", MASS_KILOGRAMS, "mdi:food", None, False], - "muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", None, False], - "physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False], - "visceralFat": ["Visceral Fat", "", "mdi:food", None, False], - "metabolicAge": ["Metabolic Age", "", "mdi:calendar-heart", None, False], - "nextAlarm": ["Next Alarm Time", None, "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True], -} diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json deleted file mode 100644 index 43b4a028290..00000000000 --- a/homeassistant/components/garmin_connect/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "garmin_connect", - "name": "Garmin Connect", - "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect_ha==0.1.6"], - "codeowners": ["@cyberjunky"], - "config_flow": true, - "iot_class": "cloud_polling" -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py deleted file mode 100644 index 96f352c75b4..00000000000 --- a/homeassistant/components/garmin_connect/sensor.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Platform for Garmin Connect integration.""" -from __future__ import annotations - -import logging - -from garminconnect_ha import ( - GarminConnectAuthenticationError, - GarminConnectConnectionError, - GarminConnectTooManyRequestsError, -) - -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo - -from .alarm_util import calculate_next_active_alarms -from .const import ATTRIBUTION, DOMAIN, GARMIN_ENTITY_LIST - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities -) -> None: - """Set up Garmin Connect sensor based on a config entry.""" - garmin_data = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.data[CONF_ID] - - try: - await garmin_data.async_update() - except ( - GarminConnectConnectionError, - GarminConnectAuthenticationError, - GarminConnectTooManyRequestsError, - ) as err: - _LOGGER.error("Error occurred during Garmin Connect Client update: %s", err) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error occurred during Garmin Connect Client update") - - entities = [] - for ( - sensor_type, - (name, unit, icon, device_class, enabled_by_default), - ) in GARMIN_ENTITY_LIST.items(): - - _LOGGER.debug( - "Registering entity: %s, %s, %s, %s, %s, %s", - sensor_type, - name, - unit, - icon, - device_class, - enabled_by_default, - ) - entities.append( - GarminConnectSensor( - garmin_data, - unique_id, - sensor_type, - name, - unit, - icon, - device_class, - enabled_by_default, - ) - ) - - async_add_entities(entities, True) - - -class GarminConnectSensor(SensorEntity): - """Representation of a Garmin Connect Sensor.""" - - def __init__( - self, - data, - unique_id, - sensor_type, - name, - unit, - icon, - device_class, - enabled_default: bool = True, - ): - """Initialize.""" - self._data = data - self._unique_id = unique_id - self._type = sensor_type - self._name = name - self._unit = unit - self._icon = icon - self._device_class = device_class - self._enabled_default = enabled_default - self._available = True - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return self._icon - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self._unique_id}_{self._type}" - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - @property - def extra_state_attributes(self): - """Return attributes for sensor.""" - if not self._data.data: - return {} - attributes = { - "source": self._data.data["source"], - "last_synced": self._data.data["lastSyncTimestampGMT"], - ATTR_ATTRIBUTION: ATTRIBUTION, - } - if self._type == "nextAlarm": - attributes["next_alarms"] = calculate_next_active_alarms( - self._data.data[self._type] - ) - return attributes - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": "Garmin Connect", - "manufacturer": "Garmin Connect", - } - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - async def async_update(self): - """Update the data from Garmin Connect.""" - if not self.enabled: - return - - await self._data.async_update() - data = self._data.data - if not data: - _LOGGER.error("Didn't receive data from Garmin Connect") - return - if data.get(self._type) is None: - _LOGGER.debug("Entity type %s not set in fetched data", self._type) - self._available = False - return - self._available = True - - if "Duration" in self._type or "Seconds" in self._type: - self._state = data[self._type] // 60 - elif "Mass" in self._type or self._type == "weight": - self._state = round((data[self._type] / 1000), 2) - elif ( - self._type == "bodyFat" or self._type == "bodyWater" or self._type == "bmi" - ): - self._state = round(data[self._type], 2) - elif self._type == "nextAlarm": - active_alarms = calculate_next_active_alarms(data[self._type]) - if active_alarms: - self._state = active_alarms[0] - else: - self._available = False - else: - self._state = data[self._type] - - _LOGGER.debug( - "Entity %s set to state %s %s", self._type, self._state, self._unit - ) diff --git a/homeassistant/components/garmin_connect/strings.json b/homeassistant/components/garmin_connect/strings.json deleted file mode 100644 index 0ec7a3ce04c..00000000000 --- a/homeassistant/components/garmin_connect/strings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "too_many_requests": "Too many requests, retry later.", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" - }, - "description": "Enter your credentials.", - "title": "Garmin Connect" - } - } - } -} diff --git a/homeassistant/components/garmin_connect/translations/ca.json b/homeassistant/components/garmin_connect/translations/ca.json deleted file mode 100644 index 73b12090fcf..00000000000 --- a/homeassistant/components/garmin_connect/translations/ca.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key::common::config_flow::abort::already_configured_account%]" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "too_many_requests": "Massa sol\u00b7licituds, torna-ho a intentar m\u00e9s tard.", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "password": "Contrasenya", - "username": "Nom d'usuari" - }, - "description": "Introdueix les teves credencials.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/cs.json b/homeassistant/components/garmin_connect/translations/cs.json deleted file mode 100644 index 86b0ce1ddef..00000000000 --- a/homeassistant/components/garmin_connect/translations/cs.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" - }, - "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "too_many_requests": "P\u0159\u00edli\u0161 mnoho po\u017eadavk\u016f, opakujte to pozd\u011bji.", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/da.json b/homeassistant/components/garmin_connect/translations/da.json deleted file mode 100644 index f664ad0e1f4..00000000000 --- a/homeassistant/components/garmin_connect/translations/da.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Denne konto er allerede konfigureret." - }, - "error": { - "cannot_connect": "Kunne ikke oprette forbindelse - pr\u00f8v igen.", - "invalid_auth": "Ugyldig godkendelse.", - "too_many_requests": "For mange anmodninger - pr\u00f8v igen senere.", - "unknown": "Uventet fejl." - }, - "step": { - "user": { - "data": { - "password": "Adgangskode", - "username": "Brugernavn" - }, - "description": "Indtast dine legitimationsoplysninger.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/de.json b/homeassistant/components/garmin_connect/translations/de.json deleted file mode 100644 index d6310595ad8..00000000000 --- a/homeassistant/components/garmin_connect/translations/de.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konto wurde bereits konfiguriert" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "password": "Passwort", - "username": "Benutzername" - }, - "description": "Gib deine Zugangsdaten ein.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/en.json b/homeassistant/components/garmin_connect/translations/en.json deleted file mode 100644 index c1b563d38f3..00000000000 --- a/homeassistant/components/garmin_connect/translations/en.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Account is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "too_many_requests": "Too many requests, retry later.", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "password": "Password", - "username": "Username" - }, - "description": "Enter your credentials.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/es-419.json b/homeassistant/components/garmin_connect/translations/es-419.json deleted file mode 100644 index 42263ce0780..00000000000 --- a/homeassistant/components/garmin_connect/translations/es-419.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta cuenta ya est\u00e1 configurada." - }, - "error": { - "cannot_connect": "No se pudo conectar, intente nuevamente.", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", - "too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.", - "unknown": "Error inesperado." - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Nombre de usuario" - }, - "description": "Ingrese sus credenciales.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/es.json b/homeassistant/components/garmin_connect/translations/es.json deleted file mode 100644 index bef92af2948..00000000000 --- a/homeassistant/components/garmin_connect/translations/es.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La cuenta ya ha sido configurada" - }, - "error": { - "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Usuario" - }, - "description": "Introduzca sus credenciales.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/et.json b/homeassistant/components/garmin_connect/translations/et.json deleted file mode 100644 index eeaefe92700..00000000000 --- a/homeassistant/components/garmin_connect/translations/et.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konto on juba seadistatud" - }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamine nurjus", - "too_many_requests": "Liiga palju taotlusi, proovi hiljem uuesti.", - "unknown": "Tundmatu viga" - }, - "step": { - "user": { - "data": { - "password": "Salas\u00f5na", - "username": "Kasutajanimi" - }, - "description": "Sisesta oma mandaat.", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/fr.json b/homeassistant/components/garmin_connect/translations/fr.json deleted file mode 100644 index ce97ccccf1b..00000000000 --- a/homeassistant/components/garmin_connect/translations/fr.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9." - }, - "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer.", - "invalid_auth": "Authentification non valide.", - "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", - "unknown": "Erreur inattendue." - }, - "step": { - "user": { - "data": { - "password": "Mot de passe", - "username": "Nom d'utilisateur" - }, - "description": "Entrez vos informations d'identification.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/he.json b/homeassistant/components/garmin_connect/translations/he.json deleted file mode 100644 index e7bab78fd58..00000000000 --- a/homeassistant/components/garmin_connect/translations/he.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, - "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "too_many_requests": "\u05d1\u05e7\u05e9\u05d5\u05ea \u05e8\u05d1\u05d5\u05ea \u05de\u05d3\u05d9, \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05e0\u05d9\u05ea \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8.", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/hu.json b/homeassistant/components/garmin_connect/translations/hu.json deleted file mode 100644 index ae518acf001..00000000000 --- a/homeassistant/components/garmin_connect/translations/hu.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" - }, - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "description": "Adja meg a hiteles\u00edt\u0151 adatait.", - "title": "Garmin Csatlakoz\u00e1s" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/id.json b/homeassistant/components/garmin_connect/translations/id.json deleted file mode 100644 index 27460757234..00000000000 --- a/homeassistant/components/garmin_connect/translations/id.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Akun sudah dikonfigurasi" - }, - "error": { - "cannot_connect": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid", - "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "password": "Kata Sandi", - "username": "Nama Pengguna" - }, - "description": "Masukkan kredensial Anda.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/it.json b/homeassistant/components/garmin_connect/translations/it.json deleted file mode 100644 index 791de295a80..00000000000 --- a/homeassistant/components/garmin_connect/translations/it.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" - }, - "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida", - "too_many_requests": "Troppe richieste, riprovare pi\u00f9 tardi.", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "password": "Password", - "username": "Nome utente" - }, - "description": "Inserisci le tue credenziali", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/ko.json b/homeassistant/components/garmin_connect/translations/ko.json deleted file mode 100644 index 4d5330a824f..00000000000 --- a/homeassistant/components/garmin_connect/translations/ko.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "too_many_requests": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/lb.json b/homeassistant/components/garmin_connect/translations/lb.json deleted file mode 100644 index 583942b1575..00000000000 --- a/homeassistant/components/garmin_connect/translations/lb.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kont ass scho konfigur\u00e9iert" - }, - "error": { - "cannot_connect": "Feeler beim verbannen", - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "too_many_requests": "Ze vill Ufroen, prob\u00e9iert sp\u00e9ider nach emol.", - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "password": "Passwuert", - "username": "Benotzernumm" - }, - "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/lv.json b/homeassistant/components/garmin_connect/translations/lv.json deleted file mode 100644 index 2c205bdd324..00000000000 --- a/homeassistant/components/garmin_connect/translations/lv.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Parole", - "username": "Lietot\u0101jv\u0101rds" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/nl.json b/homeassistant/components/garmin_connect/translations/nl.json deleted file mode 100644 index e751aaf1b5c..00000000000 --- a/homeassistant/components/garmin_connect/translations/nl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Account is al geconfigureerd" - }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", - "too_many_requests": "Te veel aanvragen, probeer het later opnieuw.", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "password": "Wachtwoord", - "username": "Gebruikersnaam" - }, - "description": "Voer uw gegevens in", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/no.json b/homeassistant/components/garmin_connect/translations/no.json deleted file mode 100644 index 41cc222bb73..00000000000 --- a/homeassistant/components/garmin_connect/translations/no.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kontoen er allerede konfigurert" - }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning", - "too_many_requests": "For mange foresp\u00f8rsler, pr\u00f8v p\u00e5 nytt senere.", - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "password": "Passord", - "username": "Brukernavn" - }, - "description": "Fyll inn legitimasjonen din.", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/pl.json b/homeassistant/components/garmin_connect/translations/pl.json deleted file mode 100644 index 715258e15f9..00000000000 --- a/homeassistant/components/garmin_connect/translations/pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", - "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/pt-BR.json b/homeassistant/components/garmin_connect/translations/pt-BR.json deleted file mode 100644 index 157ac3f0477..00000000000 --- a/homeassistant/components/garmin_connect/translations/pt-BR.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Digite suas credenciais.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/pt.json b/homeassistant/components/garmin_connect/translations/pt.json deleted file mode 100644 index 2d9b2f9e9c5..00000000000 --- a/homeassistant/components/garmin_connect/translations/pt.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Conta j\u00e1 configurada" - }, - "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "password": "Palavra-passe", - "username": "Nome de Utilizador" - }, - "description": "Introduza as suas credenciais.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/ru.json b/homeassistant/components/garmin_connect/translations/ru.json deleted file mode 100644 index 066c337309f..00000000000 --- a/homeassistant/components/garmin_connect/translations/ru.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." - }, - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/sl.json b/homeassistant/components/garmin_connect/translations/sl.json deleted file mode 100644 index 594cbffeaa7..00000000000 --- a/homeassistant/components/garmin_connect/translations/sl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ta ra\u010dun je \u017ee konfiguriran." - }, - "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova.", - "invalid_auth": "Neveljavna avtentikacija.", - "too_many_requests": "Preve\u010d zahtev, poskusite pozneje.", - "unknown": "Nepri\u010dakovana napaka." - }, - "step": { - "user": { - "data": { - "password": "Geslo", - "username": "Uporabni\u0161ko ime" - }, - "description": "Vnesite svoje poverilnice.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/sv.json b/homeassistant/components/garmin_connect/translations/sv.json deleted file mode 100644 index 0f11ab2a8b9..00000000000 --- a/homeassistant/components/garmin_connect/translations/sv.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Det h\u00e4r kontot har redan konfigurerats." - }, - "error": { - "cannot_connect": "Kunde inte ansluta, var god f\u00f6rs\u00f6k igen.", - "invalid_auth": "Ogiltig autentisering.", - "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare.", - "unknown": "Ov\u00e4ntat fel." - }, - "step": { - "user": { - "data": { - "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn" - }, - "description": "Ange dina anv\u00e4ndaruppgifter.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/tr.json b/homeassistant/components/garmin_connect/translations/tr.json deleted file mode 100644 index a83e1936fb4..00000000000 --- a/homeassistant/components/garmin_connect/translations/tr.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/uk.json b/homeassistant/components/garmin_connect/translations/uk.json deleted file mode 100644 index aef0632b0f1..00000000000 --- a/homeassistant/components/garmin_connect/translations/uk.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." - }, - "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "too_many_requests": "\u0417\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0437\u0430\u043f\u0438\u0442\u0456\u0432, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/zh-Hans.json b/homeassistant/components/garmin_connect/translations/zh-Hans.json deleted file mode 100644 index a5f4ff11f09..00000000000 --- a/homeassistant/components/garmin_connect/translations/zh-Hans.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "\u7528\u6237\u540d" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/zh-Hant.json b/homeassistant/components/garmin_connect/translations/zh-Hant.json deleted file mode 100644 index cbf928152aa..00000000000 --- a/homeassistant/components/garmin_connect/translations/zh-Hant.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u8f38\u5165\u6191\u8b49\u3002", - "title": "Garmin Connect" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 445f1cbde66..e132c81602d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -91,7 +91,6 @@ FLOWS = [ "fritzbox", "fritzbox_callmonitor", "garages_amsterdam", - "garmin_connect", "gdacs", "geofency", "geonetnz_quakes", diff --git a/mypy.ini b/mypy.ini index 2342bf3d966..409dbbdab76 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1310,9 +1310,6 @@ ignore_errors = true [mypy-homeassistant.components.freebox.*] ignore_errors = true -[mypy-homeassistant.components.garmin_connect.*] -ignore_errors = true - [mypy-homeassistant.components.geniushub.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index d3b375a53f4..b23f28caafd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -654,9 +654,6 @@ gTTS==2.2.3 # homeassistant.components.garages_amsterdam garages-amsterdam==2.1.1 -# homeassistant.components.garmin_connect -garminconnect_ha==0.1.6 - # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f0dca47aac..fd290d3bfe8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -366,9 +366,6 @@ gTTS==2.2.3 # homeassistant.components.garages_amsterdam garages-amsterdam==2.1.1 -# homeassistant.components.garmin_connect -garminconnect_ha==0.1.6 - # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f07575b61de..b5945d2a6da 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -51,7 +51,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.fortios.*", "homeassistant.components.foscam.*", "homeassistant.components.freebox.*", - "homeassistant.components.garmin_connect.*", "homeassistant.components.geniushub.*", "homeassistant.components.glances.*", "homeassistant.components.google_assistant.*", diff --git a/tests/components/garmin_connect/__init__.py b/tests/components/garmin_connect/__init__.py deleted file mode 100644 index 26de06ae0ac..00000000000 --- a/tests/components/garmin_connect/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Garmin Connect component.""" diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py deleted file mode 100644 index dd56fba9c1c..00000000000 --- a/tests/components/garmin_connect/test_config_flow.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Test the Garmin Connect config flow.""" -from unittest.mock import patch - -from garminconnect_ha import ( - GarminConnectAuthenticationError, - GarminConnectConnectionError, - GarminConnectTooManyRequestsError, -) - -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.garmin_connect.const import DOMAIN -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME - -from tests.common import MockConfigEntry - -MOCK_CONF = { - CONF_ID: "my@email.address", - CONF_USERNAME: "my@email.address", - CONF_PASSWORD: "mypassw0rd", -} - - -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == config_entries.SOURCE_USER - - -async def test_step_user(hass): - """Test registering an integration and finishing flow works.""" - with patch( - "homeassistant.components.garmin_connect.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.garmin_connect.config_flow.Garmin", - ) as garmin: - garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == MOCK_CONF - - -async def test_connection_error(hass): - """Test for connection error.""" - with patch( - "homeassistant.components.garmin_connect.Garmin.login", - side_effect=GarminConnectConnectionError("errormsg"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_authentication_error(hass): - """Test for authentication error.""" - with patch( - "homeassistant.components.garmin_connect.Garmin.login", - side_effect=GarminConnectAuthenticationError("errormsg"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} - - -async def test_toomanyrequest_error(hass): - """Test for toomanyrequests error.""" - with patch( - "homeassistant.components.garmin_connect.Garmin.login", - side_effect=GarminConnectTooManyRequestsError("errormsg"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "too_many_requests"} - - -async def test_unknown_error(hass): - """Test for unknown error.""" - with patch( - "homeassistant.components.garmin_connect.Garmin.login", - side_effect=Exception, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - - -async def test_abort_if_already_setup(hass): - """Test abort if already setup.""" - with patch( - "homeassistant.components.garmin_connect.config_flow.Garmin", - ): - entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] - ) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" From f71980a6347f1993201891722bfc0a41cd5ea2e6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 27 Jul 2021 12:56:34 -0700 Subject: [PATCH 660/818] Create stats for all sensors that have % unit and are measurement (#53576) --- homeassistant/components/sensor/recorder.py | 48 ++++++++++++--------- tests/components/sensor/test_recorder.py | 1 + 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index b5a38cfeec1..afcfe2f228d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -23,6 +23,7 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + PERCENTAGE, POWER_KILO_WATT, POWER_WATT, PRESSURE_BAR, @@ -44,7 +45,7 @@ from . import ATTR_LAST_RESET, DOMAIN _LOGGER = logging.getLogger(__name__) -DEVICE_CLASS_STATISTICS = { +DEVICE_CLASS_OR_UNIT_STATISTICS = { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, DEVICE_CLASS_ENERGY: {"sum"}, DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, @@ -52,6 +53,7 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_POWER: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + PERCENTAGE: {"mean", "min", "max"}, } # Normalized units which will be stored in the statistics table @@ -102,13 +104,19 @@ def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: entity_ids = [] for state in all_sensors: - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - state_class = state.attributes.get(ATTR_STATE_CLASS) - if not state_class or state_class != STATE_CLASS_MEASUREMENT: + if state.attributes.get(ATTR_STATE_CLASS) != STATE_CLASS_MEASUREMENT: continue - if not device_class or device_class not in DEVICE_CLASS_STATISTICS: - continue - entity_ids.append((state.entity_id, device_class)) + + if ( + key := state.attributes.get(ATTR_DEVICE_CLASS) + ) in DEVICE_CLASS_OR_UNIT_STATISTICS: + entity_ids.append((state.entity_id, key)) + + if ( + key := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + ) in DEVICE_CLASS_OR_UNIT_STATISTICS: + entity_ids.append((state.entity_id, key)) + return entity_ids @@ -158,12 +166,12 @@ def _time_weighted_average( def _normalize_states( - entity_history: list[State], device_class: str, entity_id: str + entity_history: list[State], key: str, entity_id: str ) -> tuple[str | None, list[tuple[float, State]]]: """Normalize units.""" unit = None - if device_class not in UNIT_CONVERSIONS: + if key not in UNIT_CONVERSIONS: # We're not normalizing this device class, return the state as they are fstates = [ (float(el.state), el) for el in entity_history if _is_number(el.state) @@ -182,15 +190,15 @@ def _normalize_states( fstate = float(state.state) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # Exclude unsupported units from statistics - if unit not in UNIT_CONVERSIONS[device_class]: + if unit not in UNIT_CONVERSIONS[key]: if entity_id not in WARN_UNSUPPORTED_UNIT: WARN_UNSUPPORTED_UNIT.add(entity_id) _LOGGER.warning("%s has unknown unit %s", entity_id, unit) continue - fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) + fstates.append((UNIT_CONVERSIONS[key][unit](fstate), state)) - return DEVICE_CLASS_UNITS[device_class], fstates + return DEVICE_CLASS_UNITS[key], fstates def compile_statistics( @@ -209,14 +217,14 @@ def compile_statistics( hass, start - datetime.timedelta.resolution, end, [i[0] for i in entities] ) - for entity_id, device_class in entities: - wanted_statistics = DEVICE_CLASS_STATISTICS[device_class] + for entity_id, key in entities: + wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key] if entity_id not in history_list: continue entity_history = history_list[entity_id] - unit, fstates = _normalize_states(entity_history, device_class, entity_id) + unit, fstates = _normalize_states(entity_history, key, entity_id) if not fstates: continue @@ -288,8 +296,8 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - statistic_ids = {} - for entity_id, device_class in entities: - provided_statistics = DEVICE_CLASS_STATISTICS[device_class] + for entity_id, key in entities: + provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key] if statistic_type is not None and statistic_type not in provided_statistics: continue @@ -302,14 +310,14 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if device_class not in UNIT_CONVERSIONS: + if key not in UNIT_CONVERSIONS: statistic_ids[entity_id] = native_unit continue - if native_unit not in UNIT_CONVERSIONS[device_class]: + if native_unit not in UNIT_CONVERSIONS[key]: continue - statistics_unit = DEVICE_CLASS_UNITS[device_class] + statistics_unit = DEVICE_CLASS_UNITS[key] statistic_ids[entity_id] = statistics_unit return statistic_ids diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 998cc93e629..58614e86a0e 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -44,6 +44,7 @@ TEMPERATURE_SENSOR_ATTRIBUTES = { @pytest.mark.parametrize( "device_class,unit,native_unit,mean,min,max", [ + (None, "%", "%", 16.440677, 10, 30), ("battery", "%", "%", 16.440677, 10, 30), ("battery", None, None, 16.440677, 10, 30), ("humidity", "%", "%", 16.440677, 10, 30), From f92ba75791e21372e721cd20d17d66c9c96e2f98 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 27 Jul 2021 14:11:54 -0600 Subject: [PATCH 661/818] Enforce strict typing for SimpliSafe (#53417) --- .strict-typing | 1 + .../components/simplisafe/__init__.py | 133 +++++++++++------- .../simplisafe/alarm_control_panel.py | 31 ++-- .../components/simplisafe/binary_sensor.py | 38 +++-- .../components/simplisafe/config_flow.py | 53 +++++-- homeassistant/components/simplisafe/lock.py | 25 ++-- .../components/simplisafe/manifest.json | 2 +- homeassistant/components/simplisafe/sensor.py | 10 +- mypy.ini | 11 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 211 insertions(+), 97 deletions(-) diff --git a/.strict-typing b/.strict-typing index 24d35370ca5..9cfe0b5a9a7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -84,6 +84,7 @@ homeassistant.components.scene.* homeassistant.components.select.* homeassistant.components.sensor.* homeassistant.components.shelly.* +homeassistant.components.simplisafe.* homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.ssdp.* diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index baf1bcd7b2a..0853aa3974c 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,17 +1,28 @@ """Support for SimpliSafe alarm systems.""" +from __future__ import annotations + import asyncio +from collections.abc import Awaitable +from typing import Callable, cast from uuid import UUID from simplipy import get_api +from simplipy.api import API from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, SimplipyError, ) +from simplipy.sensor.v2 import SensorV2 +from simplipy.sensor.v3 import SensorV3 +from simplipy.system import SystemNotification +from simplipy.system.v2 import SystemV2 +from simplipy.system.v3 import SystemV3 import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, @@ -109,7 +120,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_get_client_id(hass): +async def async_get_client_id(hass: HomeAssistant) -> str: """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. Note that SimpliSafe requires full, "dashed" versions of UUIDs. @@ -118,7 +129,9 @@ async def async_get_client_id(hass): return str(UUID(hass_id)) -async def async_register_base_station(hass, system, config_entry_id): +async def async_register_base_station( + hass: HomeAssistant, system: SystemV2 | SystemV3, config_entry_id: str +) -> None: """Register a new bridge.""" device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( @@ -130,11 +143,11 @@ async def async_register_base_station(hass, system, config_entry_id): ) -async def async_setup_entry(hass, config_entry): # noqa: C901 - """Set up SimpliSafe as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] - +@callback +def _async_standardize_config_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Bring a config entry up to current standards.""" if CONF_PASSWORD not in config_entry.data: raise ConfigEntryAuthFailed("Config schema change requires re-authentication") @@ -154,6 +167,14 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 if entry_updates: hass.config_entries.async_update_entry(config_entry, **entry_updates) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up SimpliSafe as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] + + _async_standardize_config_entry(hass, config_entry) + _verify_domain_control = verify_domain_control(hass, DOMAIN) client_id = await async_get_client_id(hass) @@ -183,10 +204,12 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @callback - def verify_system_exists(coro): + def verify_system_exists( + coro: Callable[..., Awaitable] + ) -> Callable[..., Awaitable]: """Log an error if a service call uses an invalid system ID.""" - async def decorator(call): + async def decorator(call: ServiceCall) -> None: """Decorate.""" system_id = int(call.data[ATTR_SYSTEM_ID]) if system_id not in simplisafe.systems: @@ -197,10 +220,10 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 return decorator @callback - def v3_only(coro): + def v3_only(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]: """Log an error if the decorated coroutine is called with a v2 system.""" - async def decorator(call): + async def decorator(call: ServiceCall) -> None: """Decorate.""" system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])] if system.version != 3: @@ -212,43 +235,40 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 @verify_system_exists @_verify_domain_control - async def clear_notifications(call): + async def clear_notifications(call: ServiceCall) -> None: """Clear all active notifications.""" system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.clear_notifications() except SimplipyError as err: LOGGER.error("Error during service call: %s", err) - return @verify_system_exists @_verify_domain_control - async def remove_pin(call): + async def remove_pin(call: ServiceCall) -> None: """Remove a PIN.""" system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) except SimplipyError as err: LOGGER.error("Error during service call: %s", err) - return @verify_system_exists @_verify_domain_control - async def set_pin(call): + async def set_pin(call: ServiceCall) -> None: """Set a PIN.""" system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) except SimplipyError as err: LOGGER.error("Error during service call: %s", err) - return @verify_system_exists @v3_only @_verify_domain_control - async def set_system_properties(call): + async def set_system_properties(call: ServiceCall) -> None: """Set one or more system parameters.""" - system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] + system = cast(SystemV3, simplisafe.systems[call.data[ATTR_SYSTEM_ID]]) try: await system.set_properties( { @@ -259,7 +279,6 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 ) except SimplipyError as err: LOGGER.error("Error during service call: %s", err) - return for service, method, schema in ( ("clear_notifications", clear_notifications, None), @@ -278,7 +297,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a SimpliSafe config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: @@ -287,7 +306,7 @@ async def async_unload_entry(hass, entry): return unload_ok -async def async_reload_entry(hass, config_entry): +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle an options update.""" await hass.config_entries.async_reload(config_entry.entry_id) @@ -295,17 +314,19 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, config_entry, api): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: API + ) -> None: """Initialize.""" self._api = api self._hass = hass - self._system_notifications = {} + self._system_notifications: dict[int, set[SystemNotification]] = {} self.config_entry = config_entry - self.coordinator = None - self.systems = {} + self.coordinator: DataUpdateCoordinator | None = None + self.systems: dict[int, SystemV2 | SystemV3] = {} @callback - def _async_process_new_notifications(self, system): + def _async_process_new_notifications(self, system: SystemV2 | SystemV3) -> None: """Act on any new system notifications.""" if self._hass.state != CoreState.running: # If HASS isn't fully running yet, it may cause the SIMPLISAFE_NOTIFICATION @@ -324,8 +345,6 @@ class SimpliSafe: LOGGER.debug("New system notifications: %s", to_add) - self._system_notifications[system.system_id].update(to_add) - for notification in to_add: text = notification.text if notification.link: @@ -341,7 +360,9 @@ class SimpliSafe: }, ) - async def async_init(self): + self._system_notifications[system.system_id] = latest_notifications + + async def async_init(self) -> None: """Initialize the data class.""" self.systems = await self._api.get_systems() for system in self.systems.values(): @@ -361,10 +382,10 @@ class SimpliSafe: update_method=self.async_update, ) - async def async_update(self): + async def async_update(self) -> None: """Get updated data from SimpliSafe.""" - async def async_update_system(system): + async def async_update_system(system: SystemV2 | SystemV3) -> None: """Update a system.""" await system.update(cached=system.version != 3) self._async_process_new_notifications(system) @@ -389,8 +410,16 @@ class SimpliSafe: class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" - def __init__(self, simplisafe, system, name, *, serial=None): + def __init__( + self, + simplisafe: SimpliSafe, + system: SystemV2 | SystemV3, + name: str, + *, + serial: str | None = None, + ) -> None: """Initialize.""" + assert simplisafe.coordinator super().__init__(simplisafe.coordinator) if serial: @@ -413,32 +442,33 @@ class SimpliSafeEntity(CoordinatorEntity): self._system = system @property - def available(self): + def available(self) -> bool: """Return whether the entity is available.""" # We can easily detect if the V3 system is offline, but no simple check exists # for the V2 system. Therefore, assuming the coordinator hasn't failed, we mark # the entity as available if: # 1. We can verify that the system is online (assuming True if we can't) # 2. We can verify that the entity is online - return ( - super().available - and self._online - and not (self._system.version == 3 and self._system.offline) - ) + if isinstance(self._system, SystemV3): + system_offline = self._system.offline + else: + system_offline = False + + return super().available and self._online and not system_offline @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Update the entity with new REST API data.""" self.async_update_from_rest_api() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() self.async_update_from_rest_api() @callback - def async_update_from_rest_api(self): + def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" raise NotImplementedError() @@ -446,13 +476,22 @@ class SimpliSafeEntity(CoordinatorEntity): class SimpliSafeBaseSensor(SimpliSafeEntity): """Define a SimpliSafe base (binary) sensor.""" - def __init__(self, simplisafe, system, sensor): + def __init__( + self, + simplisafe: SimpliSafe, + system: SystemV2 | SystemV3, + sensor: SensorV2 | SensorV3, + ) -> None: """Initialize.""" super().__init__(simplisafe, system, sensor.name, serial=sensor.serial) - self._attr_device_info["identifiers"] = {(DOMAIN, sensor.serial)} - self._attr_device_info["model"] = sensor.type.name - self._attr_device_info["name"] = sensor.name + self._attr_device_info = { + "identifiers": {(DOMAIN, sensor.serial)}, + "manufacturer": "SimpliSafe", + "model": sensor.type.name, + "name": sensor.name, + "via_device": (DOMAIN, system.serial), + } human_friendly_name = " ".join([w.title() for w in sensor.type.name.split("_")]) self._attr_name = f"{super().name} {human_friendly_name}" diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 28013b69d55..5c50d6a343e 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,8 +1,12 @@ """Support for SimpliSafe alarm control panels.""" +from __future__ import annotations + import re from simplipy.errors import SimplipyError from simplipy.system import SystemStates +from simplipy.system.v2 import SystemV2 +from simplipy.system.v3 import SystemV3 from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -13,6 +17,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CODE, STATE_ALARM_ARMED_AWAY, @@ -21,9 +26,10 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SimpliSafeEntity +from . import SimpliSafe, SimpliSafeEntity from .const import ( ATTR_ALARM_DURATION, ATTR_ALARM_VOLUME, @@ -48,7 +54,9 @@ ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up a SimpliSafe alarm control panel based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] async_add_entities( @@ -60,7 +68,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Representation of a SimpliSafe alarm.""" - def __init__(self, simplisafe, system): + def __init__(self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3) -> None: """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") @@ -91,7 +99,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_state = None @callback - def _is_code_valid(self, code, state): + def _is_code_valid(self, code: str | None, state: str) -> bool: """Validate that a code matches the required one.""" if not self._simplisafe.config_entry.options.get(CONF_CODE): return True @@ -104,7 +112,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return True - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._is_code_valid(code, STATE_ALARM_DISARMED): return @@ -118,7 +126,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_state = STATE_ALARM_DISARMED self.async_write_ha_state() - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if not self._is_code_valid(code, STATE_ALARM_ARMED_HOME): return @@ -134,7 +142,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_state = STATE_ALARM_ARMED_HOME self.async_write_ha_state() - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if not self._is_code_valid(code, STATE_ALARM_ARMED_AWAY): return @@ -151,9 +159,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self.async_write_ha_state() @callback - def async_update_from_rest_api(self): + def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" - if self._system.version == 3: + if isinstance(self._system, SystemV3): self._attr_extra_state_attributes.update( { ATTR_ALARM_DURATION: self._system.alarm_duration, @@ -175,9 +183,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): } ) - # Although system state updates are designed the come via the websocket, the - # SimpliSafe cloud can sporadically fail to send those updates as expected; so, - # just in case, we synchronize the state via the REST API, too: if self._system.state == SystemStates.alarm: self._attr_state = STATE_ALARM_TRIGGERED elif self._system.state == SystemStates.away: diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 70fa0fb0e51..1c471d10ce8 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -1,5 +1,9 @@ """Support for SimpliSafe binary sensors.""" -from simplipy.entity import EntityTypes +from __future__ import annotations + +from simplipy.entity import Entity as SimplipyEntity, EntityTypes +from simplipy.system.v2 import SystemV2 +from simplipy.system.v3 import SystemV3 from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY, @@ -11,9 +15,11 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SimpliSafeBaseSensor +from . import SimpliSafe, SimpliSafeBaseSensor from .const import DATA_CLIENT, DOMAIN, LOGGER SUPPORTED_BATTERY_SENSOR_TYPES = [ @@ -39,10 +45,13 @@ TRIGGERED_SENSOR_TYPES = { } -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up SimpliSafe binary sensors based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] - sensors = [] + + sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = [] for system in simplisafe.systems.values(): if system.version == 2: @@ -68,14 +77,20 @@ async def async_setup_entry(hass, entry, async_add_entities): class TriggeredBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): """Define a binary sensor related to whether an entity has been triggered.""" - def __init__(self, simplisafe, system, sensor, device_class): + def __init__( + self, + simplisafe: SimpliSafe, + system: SystemV2 | SystemV3, + sensor: SimplipyEntity, + device_class: str, + ) -> None: """Initialize.""" super().__init__(simplisafe, system, sensor) self._attr_device_class = device_class @callback - def async_update_from_rest_api(self): + def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" self._attr_is_on = self._sensor.triggered @@ -85,13 +100,18 @@ class BatteryBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY - def __init__(self, simplisafe, system, sensor): + def __init__( + self, + simplisafe: SimpliSafe, + system: SystemV2 | SystemV3, + sensor: SimplipyEntity, + ) -> None: """Initialize.""" super().__init__(simplisafe, system, sensor) self._attr_unique_id = f"{super().unique_id}-battery" @callback - def async_update_from_rest_api(self): + def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" self._attr_is_on = self._sensor.low_battery diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index ac31779175f..31ae125046c 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,5 +1,10 @@ """Config flow to configure the SimpliSafe component.""" +from __future__ import annotations + +from typing import Any + from simplipy import get_api +from simplipy.api import API from simplipy.errors import ( InvalidCredentialsError, PendingAuthorizationError, @@ -8,9 +13,12 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import ConfigType from . import async_get_client_id from .const import DOMAIN, LOGGER @@ -30,20 +38,25 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" - self._code = None - self._password = None - self._username = None + self._code: str | None = None + self._password: str | None = None + self._username: str | None = None @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> SimpliSafeOptionsFlowHandler: """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def _async_get_simplisafe_api(self): + async def _async_get_simplisafe_api(self) -> API: """Get an authenticated SimpliSafe API client.""" + assert self._username + assert self._password + client_id = await async_get_client_id(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass) @@ -54,7 +67,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session=websession, ) - async def _async_login_during_step(self, *, step_id, form_schema): + async def _async_login_during_step( + self, *, step_id: str, form_schema: vol.Schema + ) -> FlowResult: """Attempt to log into the API from within a config flow step.""" errors = {} @@ -84,8 +99,10 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_finish(self, user_input=None): + async def async_step_finish(self, user_input: dict[str, Any]) -> FlowResult: """Handle finish config entry setup.""" + assert self._username + existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) @@ -95,7 +112,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) - async def async_step_mfa(self, user_input=None): + async def async_step_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle multi-factor auth confirmation.""" if user_input is None: return self.async_show_form(step_id="mfa") @@ -116,14 +135,16 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_reauth(self, config): + async def async_step_reauth(self, config: ConfigType) -> FlowResult: """Handle configuration by re-auth.""" self._code = config.get(CONF_CODE) self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -136,7 +157,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA) @@ -156,11 +179,13 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): """Handle a SimpliSafe options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 982494d9b1c..e912eedb955 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,11 +1,18 @@ """Support for SimpliSafe locks.""" +from __future__ import annotations + +from typing import Any + from simplipy.errors import SimplipyError -from simplipy.lock import LockStates +from simplipy.lock import Lock, LockStates +from simplipy.system.v3 import SystemV3 from homeassistant.components.lock import LockEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SimpliSafeEntity +from . import SimpliSafe, SimpliSafeEntity from .const import DATA_CLIENT, DOMAIN, LOGGER ATTR_LOCK_LOW_BATTERY = "lock_low_battery" @@ -13,7 +20,9 @@ ATTR_JAMMED = "jammed" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up SimpliSafe locks based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] locks = [] @@ -32,13 +41,13 @@ async def async_setup_entry(hass, entry, async_add_entities): class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Define a SimpliSafe lock.""" - def __init__(self, simplisafe, system, lock): + def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None: """Initialize.""" super().__init__(simplisafe, system, lock.name, serial=lock.serial) self._lock = lock - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: dict[str, Any]) -> None: """Lock the lock.""" try: await self._lock.lock() @@ -49,7 +58,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): self._attr_is_locked = True self.async_write_ha_state() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: dict[str, Any]) -> None: """Unlock the lock.""" try: await self._lock.unlock() @@ -61,7 +70,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): self.async_write_ha_state() @callback - def async_update_from_rest_api(self): + def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" self._attr_extra_state_attributes.update( { diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 3ec1e38ad4d..8c23e575cc3 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.2"], + "requirements": ["simplisafe-python==11.0.3"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 76be7b6e4f0..149319cd5bd 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -2,14 +2,18 @@ from simplipy.entity import EntityTypes from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafeBaseSensor from .const import DATA_CLIENT, DOMAIN, LOGGER -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up SimpliSafe freeze sensors based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] sensors = [] @@ -33,6 +37,6 @@ class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): _attr_unit_of_measurement = TEMP_FAHRENHEIT @callback - def async_update_from_rest_api(self): + def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" self._attr_state = self._sensor.temperature diff --git a/mypy.ini b/mypy.ini index 409dbbdab76..613a4e3335d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -935,6 +935,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.simplisafe.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.slack.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index b23f28caafd..7cf7390d0ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2107,7 +2107,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.2 +simplisafe-python==11.0.3 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd290d3bfe8..14bf97ad2da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1156,7 +1156,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.2 +simplisafe-python==11.0.3 # homeassistant.components.slack slackclient==2.5.0 From 7ad7cdad3d28541528f04ad2795fe786c38bbd68 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 27 Jul 2021 21:19:58 +0100 Subject: [PATCH 662/818] Add Prosegur Alarms (#44679) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 + homeassistant/components/prosegur/__init__.py | 57 ++++ .../prosegur/alarm_control_panel.py | 76 ++++++ .../components/prosegur/config_flow.py | 135 ++++++++++ homeassistant/components/prosegur/const.py | 5 + .../components/prosegur/manifest.json | 13 + .../components/prosegur/strings.json | 29 ++ .../components/prosegur/translations/en.json | 29 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/prosegur/__init__.py | 1 + tests/components/prosegur/common.py | 27 ++ .../prosegur/test_alarm_control_panel.py | 120 +++++++++ tests/components/prosegur/test_config_flow.py | 247 ++++++++++++++++++ tests/components/prosegur/test_init.py | 74 ++++++ 16 files changed, 821 insertions(+) create mode 100644 homeassistant/components/prosegur/__init__.py create mode 100644 homeassistant/components/prosegur/alarm_control_panel.py create mode 100644 homeassistant/components/prosegur/config_flow.py create mode 100644 homeassistant/components/prosegur/const.py create mode 100644 homeassistant/components/prosegur/manifest.json create mode 100644 homeassistant/components/prosegur/strings.json create mode 100644 homeassistant/components/prosegur/translations/en.json create mode 100644 tests/components/prosegur/__init__.py create mode 100644 tests/components/prosegur/common.py create mode 100644 tests/components/prosegur/test_alarm_control_panel.py create mode 100644 tests/components/prosegur/test_config_flow.py create mode 100644 tests/components/prosegur/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 9b18f1a1b23..bfd74e97f71 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -390,6 +390,7 @@ homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/profiler/* @bdraco homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar +homeassistant/components/prosegur/* @dgomes homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno homeassistant/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py new file mode 100644 index 00000000000..2e4a0bd0ed5 --- /dev/null +++ b/homeassistant/components/prosegur/__init__.py @@ -0,0 +1,57 @@ +"""The Prosegur Alarm integration.""" +import logging + +from pyprosegur.auth import Auth + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client + +from .const import CONF_COUNTRY, DOMAIN + +PLATFORMS = ["alarm_control_panel"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Prosegur Alarm from a config entry.""" + try: + session = aiohttp_client.async_get_clientsession(hass) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = Auth( + session, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_COUNTRY], + ) + await hass.data[DOMAIN][entry.entry_id].login() + + except ConnectionRefusedError as error: + _LOGGER.error("Configured credential are invalid, %s", error) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": entry.data["entry_id"]}, + ) + ) + + except ConnectionError as error: + _LOGGER.error("Could not connect with Prosegur backend: %s", error) + raise ConfigEntryNotReady from error + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py new file mode 100644 index 00000000000..a61f91830c5 --- /dev/null +++ b/homeassistant/components/prosegur/alarm_control_panel.py @@ -0,0 +1,76 @@ +"""Support for Prosegur alarm control panels.""" +import logging + +from pyprosegur.auth import Auth +from pyprosegur.installation import Installation, Status + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STATE_MAPPING = { + Status.DISARMED: STATE_ALARM_DISARMED, + Status.ARMED: STATE_ALARM_ARMED_AWAY, + Status.PARTIALLY: STATE_ALARM_ARMED_HOME, + Status.ERROR_PARTIALLY: STATE_ALARM_ARMED_HOME, +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Prosegur alarm control panel platform.""" + async_add_entities( + [ProsegurAlarm(entry.data["contract"], hass.data[DOMAIN][entry.entry_id])], + update_before_add=True, + ) + + +class ProsegurAlarm(alarm.AlarmControlPanelEntity): + """Representation of a Prosegur alarm status.""" + + def __init__(self, contract: str, auth: Auth) -> None: + """Initialize the Prosegur alarm panel.""" + self._changed_by = None + + self._installation = None + self.contract = contract + self._auth = auth + + self._attr_name = f"contract {self.contract}" + self._attr_unique_id = self.contract + self._attr_supported_features = SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME + + async def async_update(self): + """Update alarm status.""" + + try: + self._installation = await Installation.retrieve(self._auth) + except ConnectionError as err: + _LOGGER.error(err) + self._attr_available = False + return + + self._attr_state = STATE_MAPPING.get(self._installation.status) + self._attr_available = True + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self._installation.disarm(self._auth) + + async def async_alarm_arm_home(self, code=None): + """Send arm away command.""" + await self._installation.arm_partially(self._auth) + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + await self._installation.arm(self._auth) diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py new file mode 100644 index 00000000000..af1ae456f12 --- /dev/null +++ b/homeassistant/components/prosegur/config_flow.py @@ -0,0 +1,135 @@ +"""Config flow for Prosegur Alarm integration.""" +import logging + +from pyprosegur.auth import COUNTRY, Auth +from pyprosegur.installation import Installation +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import aiohttp_client + +from .const import CONF_COUNTRY, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_COUNTRY): vol.In(COUNTRY.keys()), + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + try: + session = aiohttp_client.async_get_clientsession(hass) + auth = Auth( + session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY] + ) + install = await Installation.retrieve(auth) + except ConnectionRefusedError: + raise InvalidAuth from ConnectionRefusedError + except ConnectionError: + raise CannotConnect from ConnectionError + + # Info to store in the config entry. + return { + "title": f"Contract {install.contract}", + "contract": install.contract, + "username": data[CONF_USERNAME], + "password": data[CONF_PASSWORD], + "country": data[CONF_COUNTRY], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Prosegur Alarm.""" + + VERSION = 1 + entry: ConfigEntry + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info["contract"]) + self._abort_if_unique_id_configured() + + user_input["contract"] = info["contract"] + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, data): + """Handle initiation of re-authentication with Prosegur.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle re-authentication with Prosegur.""" + errors = {} + + if user_input: + try: + user_input[CONF_COUNTRY] = self.entry.data[CONF_COUNTRY] + await validate_input(self.hass, user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + errors["base"] = "unknown" + else: + data = self.entry.data.copy() + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **data, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=self.entry.data[CONF_USERNAME] + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/prosegur/const.py b/homeassistant/components/prosegur/const.py new file mode 100644 index 00000000000..b066b320a17 --- /dev/null +++ b/homeassistant/components/prosegur/const.py @@ -0,0 +1,5 @@ +"""Constants for the Prosegur Alarm integration.""" + +DOMAIN = "prosegur" + +CONF_COUNTRY = "country" diff --git a/homeassistant/components/prosegur/manifest.json b/homeassistant/components/prosegur/manifest.json new file mode 100644 index 00000000000..853324c9408 --- /dev/null +++ b/homeassistant/components/prosegur/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "prosegur", + "name": "Prosegur Alarm", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/prosegur", + "requirements": [ + "pyprosegur==0.0.5" + ], + "codeowners": [ + "@dgomes" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/prosegur/strings.json b/homeassistant/components/prosegur/strings.json new file mode 100644 index 00000000000..919628c7510 --- /dev/null +++ b/homeassistant/components/prosegur/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "country": "Country" + } + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with Prosegur account.", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/en.json b/homeassistant/components/prosegur/translations/en.json new file mode 100644 index 00000000000..a1ced2173c7 --- /dev/null +++ b/homeassistant/components/prosegur/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Re-authenticate with Prosegur account.", + "password": "Password", + "username": "Username" + } + }, + "user": { + "data": { + "country": "Country", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e132c81602d..a6c97cbbab3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -210,6 +210,7 @@ FLOWS = [ "powerwall", "profiler", "progettihwsw", + "prosegur", "ps4", "pvpc_hourly_pricing", "rachio", diff --git a/requirements_all.txt b/requirements_all.txt index 7cf7390d0ca..99b24529089 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1691,6 +1691,9 @@ pypoint==2.1.0 # homeassistant.components.profiler pyprof2calltree==1.4.5 +# homeassistant.components.prosegur +pyprosegur==0.0.5 + # homeassistant.components.ps4 pyps4-2ndscreen==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14bf97ad2da..c87c74966ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -972,6 +972,9 @@ pypoint==2.1.0 # homeassistant.components.profiler pyprof2calltree==1.4.5 +# homeassistant.components.prosegur +pyprosegur==0.0.5 + # homeassistant.components.ps4 pyps4-2ndscreen==1.2.0 diff --git a/tests/components/prosegur/__init__.py b/tests/components/prosegur/__init__.py new file mode 100644 index 00000000000..e0907bcaf9d --- /dev/null +++ b/tests/components/prosegur/__init__.py @@ -0,0 +1 @@ +"""Tests for the Prosegur Alarm integration.""" diff --git a/tests/components/prosegur/common.py b/tests/components/prosegur/common.py new file mode 100644 index 00000000000..504da3ea92a --- /dev/null +++ b/tests/components/prosegur/common.py @@ -0,0 +1,27 @@ +"""Common methods used across tests for Prosegur.""" +from homeassistant.components.prosegur import DOMAIN as PROSEGUR_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +CONTRACT = "1234abcd" + + +async def setup_platform(hass, platform): + """Set up the Prosegur platform.""" + mock_entry = MockConfigEntry( + domain=PROSEGUR_DOMAIN, + data={ + "contract": "1234abcd", + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + "country": "PT", + }, + ) + mock_entry.add_to_hass(hass) + + assert await async_setup_component(hass, PROSEGUR_DOMAIN, {}) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py new file mode 100644 index 00000000000..26e0c5f94b3 --- /dev/null +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -0,0 +1,120 @@ +"""Tests for the Prosegur alarm control panel device.""" + +from unittest.mock import AsyncMock, patch + +from pyprosegur.installation import Status +from pytest import fixture, mark + +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_UNAVAILABLE, +) +from homeassistant.helpers import entity_component + +from .common import CONTRACT, setup_platform + +PROSEGUR_ALARM_ENTITY = f"alarm_control_panel.contract_{CONTRACT}" + + +@fixture +def mock_auth(): + """Setups authentication.""" + + with patch("pyprosegur.auth.Auth.login", return_value=True): + yield + + +@fixture(params=list(Status)) +def mock_status(request): + """Mock the status of the alarm.""" + + install = AsyncMock() + install.contract = "123" + install.installationId = "1234abcd" + install.status = request.param + + with patch("pyprosegur.installation.Installation.retrieve", return_value=install): + yield + + +async def test_entity_registry(hass, mock_auth, mock_status): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, ALARM_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(PROSEGUR_ALARM_ENTITY) + # Prosegur alarm device unique_id is the contract id associated to the alarm account + assert entry.unique_id == CONTRACT + + await hass.async_block_till_done() + + state = hass.states.get(PROSEGUR_ALARM_ENTITY) + + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "contract 1234abcd" + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 3 + + +async def test_connection_error(hass, mock_auth): + """Test the alarm control panel when connection can't be made to the cloud service.""" + + install = AsyncMock() + install.arm = AsyncMock(return_value=False) + install.arm_partially = AsyncMock(return_value=True) + install.disarm = AsyncMock(return_value=True) + install.status = Status.ARMED + + with patch("pyprosegur.installation.Installation.retrieve", return_value=install): + + await setup_platform(hass, ALARM_DOMAIN) + + await hass.async_block_till_done() + + with patch( + "pyprosegur.installation.Installation.retrieve", side_effect=ConnectionError + ): + + await entity_component.async_update_entity(hass, PROSEGUR_ALARM_ENTITY) + + state = hass.states.get(PROSEGUR_ALARM_ENTITY) + assert state.state == STATE_UNAVAILABLE + + +@mark.parametrize( + "code, alarm_service, alarm_state", + [ + (Status.ARMED, SERVICE_ALARM_ARM_AWAY, STATE_ALARM_ARMED_AWAY), + (Status.PARTIALLY, SERVICE_ALARM_ARM_HOME, STATE_ALARM_ARMED_HOME), + (Status.DISARMED, SERVICE_ALARM_DISARM, STATE_ALARM_DISARMED), + ], +) +async def test_arm(hass, mock_auth, code, alarm_service, alarm_state): + """Test the alarm control panel can be set to away.""" + + install = AsyncMock() + install.arm = AsyncMock(return_value=False) + install.arm_partially = AsyncMock(return_value=True) + install.disarm = AsyncMock(return_value=True) + install.status = code + + with patch("pyprosegur.installation.Installation.retrieve", return_value=install): + await setup_platform(hass, ALARM_DOMAIN) + + await hass.services.async_call( + ALARM_DOMAIN, + alarm_service, + {ATTR_ENTITY_ID: PROSEGUR_ALARM_ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(PROSEGUR_ALARM_ENTITY) + assert state.state == alarm_state diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py new file mode 100644 index 00000000000..447baefed23 --- /dev/null +++ b/tests/components/prosegur/test_config_flow.py @@ -0,0 +1,247 @@ +"""Test the Prosegur Alarm config flow.""" +from unittest.mock import MagicMock, patch + +from pytest import mark + +from homeassistant import config_entries, setup +from homeassistant.components.prosegur.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.prosegur.const import DOMAIN +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + install = MagicMock() + install.contract = "123" + + with patch( + "homeassistant.components.prosegur.config_flow.Installation.retrieve", + return_value=install, + ), patch( + "homeassistant.components.prosegur.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Contract 123" + assert result2["data"] == { + "contract": "123", + "username": "test-username", + "password": "test-password", + "country": "PT", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyprosegur.auth.Auth", + side_effect=ConnectionRefusedError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyprosegur.installation.Installation", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_exception(hass): + """Test we handle unknown exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyprosegur.installation.Installation", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_validate_input(hass): + """Test we retrieve data from Installation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyprosegur.installation.Installation.retrieve", + return_value=MagicMock, + ) as mock_retrieve: + await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + + assert len(mock_retrieve.mock_calls) == 1 + + +async def test_reauth_flow(hass): + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + install = MagicMock() + install.contract = "123" + + with patch( + "homeassistant.components.prosegur.config_flow.Installation.retrieve", + return_value=install, + ) as mock_installation, patch( + "homeassistant.components.prosegur.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "new_password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == { + "country": "PT", + "username": "test-username", + "password": "new_password", + } + + assert len(mock_installation.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@mark.parametrize( + "exception, base_error", + [ + (CannotConnect, "cannot_connect"), + (InvalidAuth, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_reauth_flow_error(hass, exception, base_error): + """Test a reauthentication flow with errors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + "username": "test-username", + "password": "test-password", + "country": "PT", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.prosegur.config_flow.Installation.retrieve", + side_effect=exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "new_password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == base_error diff --git a/tests/components/prosegur/test_init.py b/tests/components/prosegur/test_init.py new file mode 100644 index 00000000000..e0fe596ee13 --- /dev/null +++ b/tests/components/prosegur/test_init.py @@ -0,0 +1,74 @@ +"""Tests prosegur setup.""" +from unittest.mock import MagicMock, patch + +from pytest import mark + +from homeassistant.components.prosegur import DOMAIN + +from tests.common import MockConfigEntry + + +@mark.parametrize( + "error", + [ + ConnectionRefusedError, + ConnectionError, + ], +) +async def test_setup_entry_fail_retrieve(hass, error): + """Test loading the Prosegur entry.""" + + hass.config.components.add(DOMAIN) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "test-username", + "password": "test-password", + "country": "PT", + "contract": "xpto", + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "pyprosegur.auth.Auth.login", + side_effect=error, + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + +async def test_unload_entry(hass, aioclient_mock): + """Test unloading the Prosegur entry.""" + + aioclient_mock.post( + "https://smart.prosegur.com/smart-server/ws/access/login", + json={"data": {"token": "123456789"}}, + ) + + hass.config.components.add(DOMAIN) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "test-username", + "password": "test-password", + "country": "PT", + "contract": "xpto", + }, + ) + config_entry.add_to_hass(hass) + + install = MagicMock() + install.contract = "123" + + with patch( + "homeassistant.components.prosegur.config_flow.Installation.retrieve", + return_value=install, + ): + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(config_entry.entry_id) From d0b9d8228771f724eec28b3b75683d6c9b5ddb98 Mon Sep 17 00:00:00 2001 From: Kuzj Date: Tue, 27 Jul 2021 23:29:43 +0300 Subject: [PATCH 663/818] Refactor bme280, add SPI support (#48775) * bme280 refactoring, add SPI support * isort, requirements * __init_.py add to .coveragerc * Re-run CI jobs * const.py to .coveragerc * Add support for IoT class in manifest * Keepalive * review suggestions * scan_interval with coordinator * black, isort * coordinator review suggestions * Set device_class * review suggestions * review suggestions * review suggestions * review suggestions * review suggestions * review suggestions Co-authored-by: Martin Hjelmare * add bme280spi to commented requirements * run script.gen_requirements_all * black Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + homeassistant/components/bme280/__init__.py | 97 +++++++ homeassistant/components/bme280/const.py | 46 ++++ homeassistant/components/bme280/manifest.json | 6 +- homeassistant/components/bme280/sensor.py | 252 ++++++++---------- requirements_all.txt | 3 + script/gen_requirements_all.py | 1 + 7 files changed, 272 insertions(+), 135 deletions(-) create mode 100644 homeassistant/components/bme280/const.py diff --git a/.coveragerc b/.coveragerc index 30bfb697b41..96bde517482 100644 --- a/.coveragerc +++ b/.coveragerc @@ -112,6 +112,8 @@ omit = homeassistant/components/bloomsky/* homeassistant/components/bluesound/* homeassistant/components/bluetooth_tracker/* + homeassistant/components/bme280/__init__.py + homeassistant/components/bme280/const.py homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmp280/sensor.py diff --git a/homeassistant/components/bme280/__init__.py b/homeassistant/components/bme280/__init__.py index 87de36fdf02..8de2b2ffe8b 100644 --- a/homeassistant/components/bme280/__init__.py +++ b/homeassistant/components/bme280/__init__.py @@ -1 +1,98 @@ """The bme280 component.""" +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL +from homeassistant.helpers import config_validation as cv, discovery + +from .const import ( + CONF_DELTA_TEMP, + CONF_FILTER_MODE, + CONF_I2C_ADDRESS, + CONF_I2C_BUS, + CONF_OPERATION_MODE, + CONF_OVERSAMPLING_HUM, + CONF_OVERSAMPLING_PRES, + CONF_OVERSAMPLING_TEMP, + CONF_SPI_BUS, + CONF_SPI_DEV, + CONF_T_STANDBY, + DEFAULT_DELTA_TEMP, + DEFAULT_FILTER_MODE, + DEFAULT_I2C_ADDRESS, + DEFAULT_I2C_BUS, + DEFAULT_MONITORED, + DEFAULT_NAME, + DEFAULT_OPERATION_MODE, + DEFAULT_OVERSAMPLING_HUM, + DEFAULT_OVERSAMPLING_PRES, + DEFAULT_OVERSAMPLING_TEMP, + DEFAULT_SCAN_INTERVAL, + DEFAULT_T_STANDBY, + DOMAIN, + SENSOR_TYPES, +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SPI_BUS): vol.Coerce(int), + vol.Optional(CONF_SPI_DEV): vol.Coerce(int), + vol.Optional( + CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS + ): cv.string, + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce( + int + ), + vol.Optional( + CONF_DELTA_TEMP, default=DEFAULT_DELTA_TEMP + ): vol.Coerce(float), + vol.Optional( + CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED + ): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional( + CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP + ): vol.Coerce(int), + vol.Optional( + CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES + ): vol.Coerce(int), + vol.Optional( + CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM + ): vol.Coerce(int), + vol.Optional( + CONF_OPERATION_MODE, default=DEFAULT_OPERATION_MODE + ): vol.Coerce(int), + vol.Optional( + CONF_T_STANDBY, default=DEFAULT_T_STANDBY + ): vol.Coerce(int), + vol.Optional( + CONF_FILTER_MODE, default=DEFAULT_FILTER_MODE + ): vol.Coerce(int), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up BME280 component.""" + bme280_config = config[DOMAIN] + for bme280_conf in bme280_config: + discovery_info = {SENSOR_DOMAIN: bme280_conf} + hass.async_create_task( + discovery.async_load_platform( + hass, SENSOR_DOMAIN, DOMAIN, discovery_info, config + ) + ) + return True diff --git a/homeassistant/components/bme280/const.py b/homeassistant/components/bme280/const.py new file mode 100644 index 00000000000..19dee41c855 --- /dev/null +++ b/homeassistant/components/bme280/const.py @@ -0,0 +1,46 @@ +"""Constants for the BME280 component.""" +from datetime import timedelta + +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, +) + +# Common +DOMAIN = "bme280" +CONF_OVERSAMPLING_TEMP = "oversampling_temperature" +CONF_OVERSAMPLING_PRES = "oversampling_pressure" +CONF_OVERSAMPLING_HUM = "oversampling_humidity" +CONF_T_STANDBY = "time_standby" +CONF_FILTER_MODE = "filter_mode" +DEFAULT_NAME = "BME280 Sensor" +DEFAULT_OVERSAMPLING_TEMP = 1 +DEFAULT_OVERSAMPLING_PRES = 1 +DEFAULT_OVERSAMPLING_HUM = 1 +DEFAULT_T_STANDBY = 5 +DEFAULT_FILTER_MODE = 0 +DEFAULT_SCAN_INTERVAL = 300 +SENSOR_TEMP = "temperature" +SENSOR_HUMID = "humidity" +SENSOR_PRESS = "pressure" +SENSOR_TYPES = { + SENSOR_TEMP: ["Temperature", None, DEVICE_CLASS_TEMPERATURE], + SENSOR_HUMID: ["Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY], + SENSOR_PRESS: ["Pressure", "mb", DEVICE_CLASS_PRESSURE], +} +DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) +# SPI +CONF_SPI_DEV = "spi_dev" +CONF_SPI_BUS = "spi_bus" +# I2C +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_DELTA_TEMP = "delta_temperature" +CONF_OPERATION_MODE = "operation_mode" +DEFAULT_OPERATION_MODE = 3 # Normal mode (forced mode: 2) +DEFAULT_I2C_ADDRESS = "0x76" +DEFAULT_I2C_BUS = 1 +DEFAULT_DELTA_TEMP = 0.0 diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 515e9e460d3..4c997152b5a 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -2,7 +2,11 @@ "domain": "bme280", "name": "Bosch BME280 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme280", - "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], + "requirements": [ + "i2csense==0.0.4", + "smbus-cffi==0.5.1", + "bme280spi==0.2.0" + ], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index d561d1fdddd..60ce963bf9e 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -1,166 +1,150 @@ """Support for BME280 temperature, humidity and pressure sensor.""" -from contextlib import suppress -from datetime import timedelta from functools import partial import logging -from i2csense.bme280 import BME280 # pylint: disable=import-error +from bme280spi import BME280 as BME280_spi # pylint: disable=import-error +from i2csense.bme280 import BME280 as BME280_i2c # pylint: disable=import-error import smbus -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, + CONF_SCAN_INTERVAL, TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from homeassistant.util.temperature import celsius_to_fahrenheit -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_OVERSAMPLING_TEMP = "oversampling_temperature" -CONF_OVERSAMPLING_PRES = "oversampling_pressure" -CONF_OVERSAMPLING_HUM = "oversampling_humidity" -CONF_OPERATION_MODE = "operation_mode" -CONF_T_STANDBY = "time_standby" -CONF_FILTER_MODE = "filter_mode" -CONF_DELTA_TEMP = "delta_temperature" - -DEFAULT_NAME = "BME280 Sensor" -DEFAULT_I2C_ADDRESS = "0x76" -DEFAULT_I2C_BUS = 1 -DEFAULT_OVERSAMPLING_TEMP = 1 # Temperature oversampling x 1 -DEFAULT_OVERSAMPLING_PRES = 1 # Pressure oversampling x 1 -DEFAULT_OVERSAMPLING_HUM = 1 # Humidity oversampling x 1 -DEFAULT_OPERATION_MODE = 3 # Normal mode (forced mode: 2) -DEFAULT_T_STANDBY = 5 # Tstandby 5ms -DEFAULT_FILTER_MODE = 0 # Filter off -DEFAULT_DELTA_TEMP = 0.0 - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) - -SENSOR_TEMP = "temperature" -SENSOR_HUMID = "humidity" -SENSOR_PRESS = "pressure" -SENSOR_TYPES = { - SENSOR_TEMP: ["Temperature", None, DEVICE_CLASS_TEMPERATURE], - SENSOR_HUMID: ["Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY], - SENSOR_PRESS: ["Pressure", "mb", DEVICE_CLASS_PRESSURE], -} -DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP - ): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES - ): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM - ): vol.Coerce(int), - vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_OPERATION_MODE): vol.Coerce( - int - ), - vol.Optional(CONF_T_STANDBY, default=DEFAULT_T_STANDBY): vol.Coerce(int), - vol.Optional(CONF_FILTER_MODE, default=DEFAULT_FILTER_MODE): vol.Coerce(int), - vol.Optional(CONF_DELTA_TEMP, default=DEFAULT_DELTA_TEMP): vol.Coerce(float), - } +from .const import ( + CONF_DELTA_TEMP, + CONF_FILTER_MODE, + CONF_I2C_ADDRESS, + CONF_I2C_BUS, + CONF_OPERATION_MODE, + CONF_OVERSAMPLING_HUM, + CONF_OVERSAMPLING_PRES, + CONF_OVERSAMPLING_TEMP, + CONF_SPI_BUS, + CONF_SPI_DEV, + CONF_T_STANDBY, + DOMAIN, + MIN_TIME_BETWEEN_UPDATES, + SENSOR_HUMID, + SENSOR_PRESS, + SENSOR_TEMP, + SENSOR_TYPES, ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME280 sensor.""" - + if discovery_info is None: + return SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit - name = config[CONF_NAME] - i2c_address = config[CONF_I2C_ADDRESS] - - bus = smbus.SMBus(config[CONF_I2C_BUS]) - sensor = await hass.async_add_executor_job( - partial( - BME280, - bus, - i2c_address, - osrs_t=config[CONF_OVERSAMPLING_TEMP], - osrs_p=config[CONF_OVERSAMPLING_PRES], - osrs_h=config[CONF_OVERSAMPLING_HUM], - mode=config[CONF_OPERATION_MODE], - t_sb=config[CONF_T_STANDBY], - filter_mode=config[CONF_FILTER_MODE], - delta_temp=config[CONF_DELTA_TEMP], - logger=_LOGGER, - ) - ) - if not sensor.sample_ok: - _LOGGER.error("BME280 sensor not detected at %s", i2c_address) - return False - - sensor_handler = await hass.async_add_executor_job(BME280Handler, sensor) - - dev = [] - with suppress(KeyError): - for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append( - BME280Sensor(sensor_handler, variable, SENSOR_TYPES[variable][1], name) + sensor_conf = discovery_info[SENSOR_DOMAIN] + name = sensor_conf[CONF_NAME] + scan_interval = max(sensor_conf[CONF_SCAN_INTERVAL], MIN_TIME_BETWEEN_UPDATES) + if CONF_SPI_BUS in sensor_conf and CONF_SPI_DEV in sensor_conf: + spi_dev = sensor_conf[CONF_SPI_DEV] + spi_bus = sensor_conf[CONF_SPI_BUS] + _LOGGER.debug("BME280 sensor initialize at %s.%s", spi_bus, spi_dev) + sensor = await hass.async_add_executor_job( + partial( + BME280_spi, + t_mode=sensor_conf[CONF_OVERSAMPLING_TEMP], + p_mode=sensor_conf[CONF_OVERSAMPLING_PRES], + h_mode=sensor_conf[CONF_OVERSAMPLING_HUM], + standby=sensor_conf[CONF_T_STANDBY], + filter=sensor_conf[CONF_FILTER_MODE], + spi_bus=sensor_conf[CONF_SPI_BUS], + spi_dev=sensor_conf[CONF_SPI_DEV], ) + ) + if not sensor.sample_ok: + _LOGGER.error("BME280 sensor not detected at %s.%s", spi_bus, spi_dev) + return + else: + i2c_address = sensor_conf[CONF_I2C_ADDRESS] + bus = smbus.SMBus(sensor_conf[CONF_I2C_BUS]) + sensor = await hass.async_add_executor_job( + partial( + BME280_i2c, + bus, + i2c_address, + osrs_t=sensor_conf[CONF_OVERSAMPLING_TEMP], + osrs_p=sensor_conf[CONF_OVERSAMPLING_PRES], + osrs_h=sensor_conf[CONF_OVERSAMPLING_HUM], + mode=sensor_conf[CONF_OPERATION_MODE], + t_sb=sensor_conf[CONF_T_STANDBY], + filter_mode=sensor_conf[CONF_FILTER_MODE], + delta_temp=sensor_conf[CONF_DELTA_TEMP], + ) + ) + if not sensor.sample_ok: + _LOGGER.error("BME280 sensor not detected at %s", i2c_address) + return - async_add_entities(dev, True) + async def async_update_data(): + await hass.async_add_executor_job(sensor.update) + if not sensor.sample_ok: + raise UpdateFailed(f"Bad update of sensor {name}") + return sensor + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=scan_interval, + ) + await coordinator.async_refresh() + entities = [] + for condition in sensor_conf[CONF_MONITORED_CONDITIONS]: + entities.append( + BME280Sensor( + condition, + SENSOR_TYPES[condition][1], + name, + coordinator, + ) + ) + async_add_entities(entities, True) -class BME280Handler: - """BME280 sensor working in i2C bus.""" - - def __init__(self, sensor): - """Initialize the sensor handler.""" - self.sensor = sensor - self.update(True) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, first_reading=False): - """Read sensor data.""" - self.sensor.update(first_reading) - - -class BME280Sensor(SensorEntity): +class BME280Sensor(CoordinatorEntity, SensorEntity): """Implementation of the BME280 sensor.""" - def __init__(self, bme280_client, sensor_type, temp_unit, name): + def __init__(self, sensor_type, temp_unit, name, coordinator): """Initialize the sensor.""" + super().__init__(coordinator) self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" - self.bme280_client = bme280_client self.temp_unit = temp_unit self.type = sensor_type self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] - async def async_update(self): - """Get the latest data from the BME280 and update the states.""" - await self.hass.async_add_executor_job(self.bme280_client.update) - if self.bme280_client.sensor.sample_ok: - if self.type == SENSOR_TEMP: - if self.temp_unit == TEMP_FAHRENHEIT: - self._attr_state = round(celsius_to_fahrenheit(self.state), 2) - else: - self._attr_state = round(self.bme280_client.sensor.temperature, 2) - elif self.type == SENSOR_HUMID: - self._attr_state = round(self.bme280_client.sensor.humidity, 1) - elif self.type == SENSOR_PRESS: - self._attr_state = round(self.bme280_client.sensor.pressure, 1) - else: - _LOGGER.warning("Bad update of sensor.%s", self.name) + @property + def state(self): + """Return the state of the sensor.""" + if self.type == SENSOR_TEMP: + temperature = round(self.coordinator.data.temperature, 1) + if self.temp_unit == TEMP_FAHRENHEIT: + temperature = round(celsius_to_fahrenheit(temperature), 1) + state = temperature + elif self.type == SENSOR_HUMID: + state = round(self.coordinator.data.humidity, 1) + elif self.type == SENSOR_PRESS: + state = round(self.coordinator.data.pressure, 1) + return state + + @property + def should_poll(self) -> bool: + """Return False if entity should not poll.""" + return False diff --git a/requirements_all.txt b/requirements_all.txt index 99b24529089..7fd36f3b516 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -391,6 +391,9 @@ blockchain==1.4.4 # homeassistant.components.miflora # bluepy==1.3.0 +# homeassistant.components.bme280 +# bme280spi==0.2.0 + # homeassistant.components.bme680 # bme680==1.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4fd96cb1b04..57bb2b339aa 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -19,6 +19,7 @@ COMMENT_REQUIREMENTS = ( "beewi_smartclim", # depends on bluepy "blinkt", "bluepy", + "bme280spi", "bme680", "decora", "decora_wifi", From 9806bda272bd855f27a2ebdda97e1725474ad03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 27 Jul 2021 22:43:05 +0200 Subject: [PATCH 664/818] Rename snapshot -> backup (#51629) --- homeassistant/components/hassio/__init__.py | 57 +++++++++++++----- homeassistant/components/hassio/http.py | 9 ++- homeassistant/components/hassio/services.yaml | 58 +++++++++++++++++-- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/addon.py | 14 ++--- tests/components/hassio/test_http.py | 20 +++---- tests/components/hassio/test_init.py | 43 ++++++++++---- tests/components/hassio/test_websocket_api.py | 4 +- tests/components/zwave_js/conftest.py | 12 ++-- tests/components/zwave_js/test_init.py | 44 +++++++------- 10 files changed, 181 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6c71f2eb042..4541685061a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -89,6 +89,8 @@ SERVICE_HOST_SHUTDOWN = "host_shutdown" SERVICE_HOST_REBOOT = "host_reboot" SERVICE_SNAPSHOT_FULL = "snapshot_full" SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" +SERVICE_BACKUP_FULL = "backup_full" +SERVICE_BACKUP_PARTIAL = "backup_partial" SERVICE_RESTORE_FULL = "restore_full" SERVICE_RESTORE_PARTIAL = "restore_partial" @@ -101,11 +103,11 @@ SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend( {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)} ) -SCHEMA_SNAPSHOT_FULL = vol.Schema( +SCHEMA_BACKUP_FULL = vol.Schema( {vol.Optional(ATTR_NAME): cv.string, vol.Optional(ATTR_PASSWORD): cv.string} ) -SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend( +SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( { vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), @@ -113,7 +115,12 @@ SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend( ) SCHEMA_RESTORE_FULL = vol.Schema( - {vol.Required(ATTR_SNAPSHOT): cv.slug, vol.Optional(ATTR_PASSWORD): cv.string} + { + vol.Exclusive(ATTR_SLUG, ATTR_SLUG): cv.slug, + vol.Exclusive(ATTR_SNAPSHOT, ATTR_SLUG): cv.slug, + vol.Optional(ATTR_PASSWORD): cv.string, + }, + cv.has_at_least_one_key(ATTR_SLUG, ATTR_SNAPSHOT), ) SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( @@ -133,25 +140,32 @@ MAP_SERVICE_API = { SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False), SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False), SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False), - SERVICE_SNAPSHOT_FULL: ("/snapshots/new/full", SCHEMA_SNAPSHOT_FULL, 300, True), - SERVICE_SNAPSHOT_PARTIAL: ( - "/snapshots/new/partial", - SCHEMA_SNAPSHOT_PARTIAL, + SERVICE_BACKUP_FULL: ("/backups/new/full", SCHEMA_BACKUP_FULL, 300, True), + SERVICE_BACKUP_PARTIAL: ( + "/backups/new/partial", + SCHEMA_BACKUP_PARTIAL, 300, True, ), SERVICE_RESTORE_FULL: ( - "/snapshots/{snapshot}/restore/full", + "/backups/{slug}/restore/full", SCHEMA_RESTORE_FULL, 300, True, ), SERVICE_RESTORE_PARTIAL: ( - "/snapshots/{snapshot}/restore/partial", + "/backups/{slug}/restore/partial", SCHEMA_RESTORE_PARTIAL, 300, True, ), + SERVICE_SNAPSHOT_FULL: ("/backups/new/full", SCHEMA_BACKUP_FULL, 300, True), + SERVICE_SNAPSHOT_PARTIAL: ( + "/backups/new/partial", + SCHEMA_BACKUP_PARTIAL, + 300, + True, + ), } @@ -272,16 +286,16 @@ async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict @bind_hass @api_data -async def async_create_snapshot( +async def async_create_backup( hass: HomeAssistant, payload: dict, partial: bool = False ) -> dict: - """Create a full or partial snapshot. + """Create a full or partial backup. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] - snapshot_type = "partial" if partial else "full" - command = f"/snapshots/new/{snapshot_type}" + backup_type = "partial" if partial else "full" + command = f"/backups/new/{backup_type}" return await hassio.send_command(command, payload=payload, timeout=None) @@ -453,9 +467,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_service_handler(service): """Handle service calls for Hass.io.""" api_command = MAP_SERVICE_API[service.service][0] + if "snapshot" in service.service: + _LOGGER.warning( + "The service '%s' is deprecated and will be removed in Home Assistant 2021.10, use '%s' instead", + service.service, + service.service.replace("snapshot", "backup"), + ) data = service.data.copy() addon = data.pop(ATTR_ADDON, None) + slug = data.pop(ATTR_SLUG, None) snapshot = data.pop(ATTR_SNAPSHOT, None) + if snapshot is not None: + _LOGGER.warning( + "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.10, use 'slug' instead" + ) + slug = snapshot + payload = None # Pass data to Hass.io API @@ -467,12 +494,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Call API try: await hassio.send_command( - api_command.format(addon=addon, snapshot=snapshot), + api_command.format(addon=addon, slug=slug), payload=payload, timeout=MAP_SERVICE_API[service.service][2], ) except HassioAPIError as err: - _LOGGER.error("Error on Hass.io API: %s", err) + _LOGGER.error("Error on Supervisor API: %s", err) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 47131b80de3..302cc00bb9f 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -29,6 +29,9 @@ NO_TIMEOUT = re.compile( r"|hassos/update/cli" r"|supervisor/update" r"|addons/[^/]+/(?:update|install|rebuild)" + r"|backups/.+/full" + r"|backups/.+/partial" + r"|backups/[^/]+/(?:upload|download)" r"|snapshots/.+/full" r"|snapshots/.+/partial" r"|snapshots/[^/]+/(?:upload|download)" @@ -36,7 +39,7 @@ NO_TIMEOUT = re.compile( ) NO_AUTH_ONBOARDING = re.compile( - r"^(?:" r"|supervisor/logs" r"|snapshots/[^/]+/.+" r")$" + r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r"|snapshots/[^/]+/.+" r")$" ) NO_AUTH = re.compile( @@ -81,13 +84,13 @@ class HassIOView(HomeAssistantView): client_timeout = 10 data = None headers = _init_header(request) - if path == "snapshots/new/upload": + if path in ("snapshots/new/upload", "backups/new/upload"): # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access - # Snapshots are big, so we need to adjust the allowed size + # Backups are big, so we need to adjust the allowed size request._client_max_size = ( # pylint: disable=protected-access MAX_UPLOAD_SIZE ) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 0652b65d6e2..38d78984ddc 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -67,13 +67,13 @@ host_shutdown: description: Poweroff the host system. snapshot_full: - name: Create a full snapshot. - description: Create a full snapshot. + name: Create a full backup. + description: Create a full backup (deprecated, use backup_full instead). fields: name: name: Name description: Optional or it will be the current date and time. - example: "Snapshot 1" + example: "backup 1" selector: text: password: @@ -84,8 +84,8 @@ snapshot_full: text: snapshot_partial: - name: Create a partial snapshot. - description: Create a partial snapshot. + name: Create a partial backup. + description: Create a partial backup (deprecated, use backup_partial instead). fields: addons: name: Add-ons @@ -102,7 +102,53 @@ snapshot_partial: name: name: Name description: Optional or it will be the current date and time. - example: "Partial Snapshot 1" + example: "Partial backup 1" + selector: + text: + password: + name: Password + description: Optional password. + example: "password" + selector: + text: + +backup_full: + name: Create a full backup. + description: Create a full backup. + fields: + name: + name: Name + description: Optional or it will be the current date and time. + example: "backup 1" + selector: + text: + password: + name: Password + description: Optional password. + example: "password" + selector: + text: + +backup_partial: + name: Create a partial backup. + description: Create a partial backup. + fields: + addons: + name: Add-ons + description: Optional list of addon slugs. + example: ["core_ssh", "core_samba", "core_mosquitto"] + selector: + object: + folders: + name: Folders + description: Optional list of directories. + example: ["homeassistant", "share"] + selector: + object: + name: + name: Name + description: Optional or it will be the current date and time. + example: "Partial backup 1" selector: text: password: diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6cd0104e298..6320efddb60 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -547,7 +547,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: LOGGER.error(err) return try: - await addon_manager.async_create_snapshot() + await addon_manager.async_create_backup() except AddonError as err: LOGGER.error(err) return diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index a0caaa15488..29ae887b4bc 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -8,7 +8,7 @@ from functools import partial from typing import Any, Callable, TypeVar, cast from homeassistant.components.hassio import ( - async_create_snapshot, + async_create_backup, async_get_addon_discovery_info, async_get_addon_info, async_install_addon, @@ -202,7 +202,7 @@ class AddonManager: if not addon_info.update_available: return - await self.async_create_snapshot() + await self.async_create_backup() await async_update_addon(self._hass, ADDON_SLUG) @callback @@ -289,14 +289,14 @@ class AddonManager: ) return self._start_task - @api_error("Failed to create a snapshot of the Z-Wave JS add-on.") - async def async_create_snapshot(self) -> None: - """Create a partial snapshot of the Z-Wave JS add-on.""" + @api_error("Failed to create a backup of the Z-Wave JS add-on.") + async def async_create_backup(self) -> None: + """Create a partial backup of the Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() name = f"addon_{ADDON_SLUG}_{addon_info.version}" - LOGGER.debug("Creating snapshot: %s", name) - await async_create_snapshot( + LOGGER.debug("Creating backup: %s", name) + await async_create_backup( self._hass, {"name": name, "addons": [ADDON_SLUG]}, partial=True, diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ff1c348a37b..fc4bb3e6a0d 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -132,13 +132,13 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo assert req_headers["X-Hass-Is-Admin"] == "1" -async def test_snapshot_upload_headers(hassio_client, aioclient_mock): - """Test that we forward the full header for snapshot upload.""" +async def test_backup_upload_headers(hassio_client, aioclient_mock, caplog): + """Test that we forward the full header for backup upload.""" content_type = "multipart/form-data; boundary='--webkit'" - aioclient_mock.get("http://127.0.0.1/snapshots/new/upload") + aioclient_mock.get("http://127.0.0.1/backups/new/upload") resp = await hassio_client.get( - "/api/hassio/snapshots/new/upload", headers={"Content-Type": content_type} + "/api/hassio/backups/new/upload", headers={"Content-Type": content_type} ) # Check we got right response @@ -150,18 +150,18 @@ async def test_snapshot_upload_headers(hassio_client, aioclient_mock): assert req_headers["Content-Type"] == content_type -async def test_snapshot_download_headers(hassio_client, aioclient_mock): - """Test that we forward the full header for snapshot download.""" +async def test_backup_download_headers(hassio_client, aioclient_mock): + """Test that we forward the full header for backup download.""" content_disposition = "attachment; filename=test.tar" aioclient_mock.get( - "http://127.0.0.1/snapshots/slug/download", + "http://127.0.0.1/backups/slug/download", headers={ "Content-Length": "50000000", "Content-Disposition": content_disposition, }, ) - resp = await hassio_client.get("/api/hassio/snapshots/slug/download") + resp = await hassio_client.get("/api/hassio/backups/slug/download") # Check we got right response assert resp.status == 200 @@ -174,9 +174,9 @@ async def test_snapshot_download_headers(hassio_client, aioclient_mock): def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") - assert _need_auth(hass, "snapshots/new/upload") + assert _need_auth(hass, "backups/new/upload") assert _need_auth(hass, "supervisor/logs") hass.data["onboarding"] = False - assert not _need_auth(hass, "snapshots/new/upload") + assert not _need_auth(hass, "backups/new/upload") assert not _need_auth(hass, "supervisor/logs") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 8377e5287d0..910ed12cb52 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -303,11 +303,13 @@ async def test_service_register(hassio_env, hass): assert hass.services.has_service("hassio", "host_reboot") assert hass.services.has_service("hassio", "snapshot_full") assert hass.services.has_service("hassio", "snapshot_partial") + assert hass.services.has_service("hassio", "backup_full") + assert hass.services.has_service("hassio", "backup_partial") assert hass.services.has_service("hassio", "restore_full") assert hass.services.has_service("hassio", "restore_partial") -async def test_service_calls(hassio_env, hass, aioclient_mock): +async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): """Call service and check the API calls behind that.""" assert await async_setup_component(hass, "hassio", {}) @@ -318,13 +320,13 @@ async def test_service_calls(hassio_env, hass, aioclient_mock): aioclient_mock.post("http://127.0.0.1/addons/test/stdin", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/host/shutdown", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/host/reboot", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/snapshots/new/full", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/snapshots/new/partial", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/backups/new/full", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/backups/new/partial", json={"result": "ok"}) aioclient_mock.post( - "http://127.0.0.1/snapshots/test/restore/full", json={"result": "ok"} + "http://127.0.0.1/backups/test/restore/full", json={"result": "ok"} ) aioclient_mock.post( - "http://127.0.0.1/snapshots/test/restore/partial", json={"result": "ok"} + "http://127.0.0.1/backups/test/restore/partial", json={"result": "ok"} ) await hass.services.async_call("hassio", "addon_start", {"addon": "test"}) @@ -345,27 +347,48 @@ async def test_service_calls(hassio_env, hass, aioclient_mock): assert aioclient_mock.call_count == 10 + await hass.services.async_call("hassio", "backup_full", {}) + await hass.services.async_call( + "hassio", + "backup_partial", + {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, + ) await hass.services.async_call("hassio", "snapshot_full", {}) await hass.services.async_call( "hassio", "snapshot_partial", - {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, + {"addons": ["test"], "folders": ["ssl"]}, ) await hass.async_block_till_done() + assert ( + "The service 'snapshot_full' is deprecated and will be removed in Home Assistant 2021.10, use 'backup_full' instead" + in caplog.text + ) + assert ( + "The service 'snapshot_partial' is deprecated and will be removed in Home Assistant 2021.10, use 'backup_partial' instead" + in caplog.text + ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[-1][2] == { + assert aioclient_mock.call_count == 14 + assert aioclient_mock.mock_calls[-3][2] == { "addons": ["test"], "folders": ["ssl"], "password": "123456", } + await hass.services.async_call("hassio", "restore_full", {"slug": "test"}) await hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) + await hass.async_block_till_done() + assert ( + "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.10, use 'slug' instead" + in caplog.text + ) + await hass.services.async_call( "hassio", "restore_partial", { - "snapshot": "test", + "slug": "test", "homeassistant": False, "addons": ["test"], "folders": ["ssl"], @@ -374,7 +397,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 14 + assert aioclient_mock.call_count == 17 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5278d2cbb91..5578194b87c 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -61,7 +61,7 @@ async def test_websocket_supervisor_api( assert await async_setup_component(hass, "hassio", {}) websocket_client = await hass_ws_client(hass) aioclient_mock.post( - "http://127.0.0.1/snapshots/new/partial", + "http://127.0.0.1/backups/new/partial", json={"result": "ok", "data": {"slug": "sn_slug"}}, ) @@ -69,7 +69,7 @@ async def test_websocket_supervisor_api( { WS_ID: 1, WS_TYPE: WS_TYPE_API, - ATTR_ENDPOINT: "/snapshots/new/partial", + ATTR_ENDPOINT: "/backups/new/partial", ATTR_METHOD: "post", } ) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index bfacd36301d..9dc8490b314 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -166,13 +166,13 @@ def uninstall_addon_fixture(): yield uninstall_addon -@pytest.fixture(name="create_shapshot") -def create_snapshot_fixture(): - """Mock create snapshot.""" +@pytest.fixture(name="create_backup") +def create_backup_fixture(): + """Mock create backup.""" with patch( - "homeassistant.components.zwave_js.addon.async_create_snapshot" - ) as create_shapshot: - yield create_shapshot + "homeassistant.components.zwave_js.addon.async_create_backup" + ) as create_backup: + yield create_backup @pytest.fixture(name="controller_state", scope="session") diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 0b9009cd1d7..447b052b8c0 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -365,8 +365,8 @@ async def test_addon_options_changed( @pytest.mark.parametrize( - "addon_version, update_available, update_calls, snapshot_calls, " - "update_addon_side_effect, create_shapshot_side_effect", + "addon_version, update_available, update_calls, backup_calls, " + "update_addon_side_effect, create_backup_side_effect", [ ("1.0", True, 1, 1, None, None), ("1.0", False, 0, 0, None, None), @@ -380,15 +380,15 @@ async def test_update_addon( addon_info, addon_installed, addon_running, - create_shapshot, + create_backup, update_addon, addon_options, addon_version, update_available, update_calls, - snapshot_calls, + backup_calls, update_addon_side_effect, - create_shapshot_side_effect, + create_backup_side_effect, ): """Test update the Z-Wave JS add-on during entry setup.""" device = "/test" @@ -397,7 +397,7 @@ async def test_update_addon( addon_options["network_key"] = network_key addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available - create_shapshot.side_effect = create_shapshot_side_effect + create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect client.connect.side_effect = InvalidServerVersion("Invalid version") entry = MockConfigEntry( @@ -416,7 +416,7 @@ async def test_update_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert create_shapshot.call_count == snapshot_calls + assert create_backup.call_count == backup_calls assert update_addon.call_count == update_calls @@ -469,7 +469,7 @@ async def test_stop_addon( async def test_remove_entry( - hass, addon_installed, stop_addon, create_shapshot, uninstall_addon, caplog + hass, addon_installed, stop_addon, create_backup, uninstall_addon, caplog ): """Test remove the config entry.""" # test successful remove without created add-on @@ -500,8 +500,8 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_shapshot.call_count == 1 - assert create_shapshot.call_args == call( + assert create_backup.call_count == 1 + assert create_backup.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, @@ -511,7 +511,7 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() - create_shapshot.reset_mock() + create_backup.reset_mock() uninstall_addon.reset_mock() # test add-on stop failure @@ -523,27 +523,27 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_shapshot.call_count == 0 + assert create_backup.call_count == 0 assert uninstall_addon.call_count == 0 assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the Z-Wave JS add-on" in caplog.text stop_addon.side_effect = None stop_addon.reset_mock() - create_shapshot.reset_mock() + create_backup.reset_mock() uninstall_addon.reset_mock() - # test create snapshot failure + # test create backup failure entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - create_shapshot.side_effect = HassioAPIError() + create_backup.side_effect = HassioAPIError() await hass.config_entries.async_remove(entry.entry_id) assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_shapshot.call_count == 1 - assert create_shapshot.call_args == call( + assert create_backup.call_count == 1 + assert create_backup.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, @@ -551,10 +551,10 @@ async def test_remove_entry( assert uninstall_addon.call_count == 0 assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text - create_shapshot.side_effect = None + assert "Failed to create a backup of the Z-Wave JS add-on" in caplog.text + create_backup.side_effect = None stop_addon.reset_mock() - create_shapshot.reset_mock() + create_backup.reset_mock() uninstall_addon.reset_mock() # test add-on uninstall failure @@ -566,8 +566,8 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_shapshot.call_count == 1 - assert create_shapshot.call_args == call( + assert create_backup.call_count == 1 + assert create_backup.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, From ac837cd76e93270b7b67bffa17349b5f885b46be Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 22:51:32 +0200 Subject: [PATCH 665/818] Use EntityDescription - ondilo_ico (#53579) --- homeassistant/components/ondilo_ico/sensor.py | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 122b4154892..26a61ddfe4c 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -3,11 +3,10 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import NamedTuple from ondilo import OndiloError -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, @@ -25,60 +24,58 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN - -class OndiloIOCSensorMetadata(NamedTuple): - """Sensor metadata for an individual Ondilo IOC sensor.""" - - name: str - unit_of_measurement: str | None - icon: str | None - device_class: str | None - - -SENSOR_TYPES: dict[str, OndiloIOCSensorMetadata] = { - "temperature": OndiloIOCSensorMetadata( - "Temperature", +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="temperature", + name="Temperature", unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, ), - "orp": OndiloIOCSensorMetadata( - "Oxydo Reduction Potential", + SensorEntityDescription( + key="orp", + name="Oxydo Reduction Potential", unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", device_class=None, ), - "ph": OndiloIOCSensorMetadata( - "pH", + SensorEntityDescription( + key="ph", + name="pH", unit_of_measurement=None, icon="mdi:pool", device_class=None, ), - "tds": OndiloIOCSensorMetadata( - "TDS", + SensorEntityDescription( + key="tds", + name="TDS", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:pool", device_class=None, ), - "battery": OndiloIOCSensorMetadata( - "Battery", + SensorEntityDescription( + key="battery", + name="Battery", unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_BATTERY, ), - "rssi": OndiloIOCSensorMetadata( - "RSSI", + SensorEntityDescription( + key="rssi", + name="RSSI", unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), - "salt": OndiloIOCSensorMetadata( - "Salt", + SensorEntityDescription( + key="salt", + name="Salt", unit_of_measurement="mg/L", icon="mdi:pool", device_class=None, ), -} +) + SCAN_INTERVAL = timedelta(hours=1) _LOGGER = logging.getLogger(__name__) @@ -116,9 +113,14 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [] for poolidx, pool in enumerate(coordinator.data): - for sensor_idx, sensor in enumerate(pool["sensors"]): - if sensor["data_type"] in SENSOR_TYPES: - entities.append(OndiloICO(coordinator, poolidx, sensor_idx)) + entities.extend( + [ + OndiloICO(coordinator, poolidx, description) + for sensor in pool["sensors"] + for description in SENSOR_TYPES + if description.key == sensor["data_type"] + ] + ) async_add_entities(entities) @@ -127,22 +129,21 @@ class OndiloICO(CoordinatorEntity, SensorEntity): """Representation of a Sensor.""" def __init__( - self, coordinator: DataUpdateCoordinator, poolidx: int, sensor_idx: int + self, + coordinator: DataUpdateCoordinator, + poolidx: int, + description: SensorEntityDescription, ) -> None: """Initialize sensor entity with data from coordinator.""" super().__init__(coordinator) + self.entity_description = description self._poolid = self.coordinator.data[poolidx]["id"] pooldata = self._pooldata() - self._data_type = pooldata["sensors"][sensor_idx]["data_type"] - self._unique_id = f"{pooldata['ICO']['serial_number']}-{self._data_type}" + self._unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}" self._device_name = pooldata["name"] - metadata = SENSOR_TYPES[self._data_type] - self._name = f"{self._device_name} {metadata.name}" - self._attr_device_class = metadata.device_class - self._attr_icon = metadata.icon - self._attr_unit_of_measurement = metadata.unit_of_measurement + self._name = f"{self._device_name} {description.name}" def _pooldata(self): """Get pool data dict.""" @@ -157,7 +158,7 @@ class OndiloICO(CoordinatorEntity, SensorEntity): ( data_type for data_type in self._pooldata()["sensors"] - if data_type["data_type"] == self._data_type + if data_type["data_type"] == self.entity_description.key ), None, ) From dd849c4eabd8843bd7f14ff8ed75aa06762739b0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 22:52:50 +0200 Subject: [PATCH 666/818] Use EntityDescription - dwd_weather_warnings (#53580) --- .../components/dwd_weather_warnings/sensor.py | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 78fa9bd8552..428ed3ab427 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -9,13 +9,19 @@ Unwetterwarnungen (Stufe 3) Warnungen vor markantem Wetter (Stufe 2) Wetterwarnungen (Stufe 1) """ +from __future__ import annotations + from datetime import timedelta import logging from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -48,18 +54,21 @@ ADVANCE_WARNING_SENSOR = "advance_warning_level" SCAN_INTERVAL = timedelta(minutes=15) -MONITORED_CONDITIONS = { - CURRENT_WARNING_SENSOR: [ - "Current Warning Level", - None, - "mdi:close-octagon-outline", - ], - ADVANCE_WARNING_SENSOR: [ - "Advance Warning Level", - None, - "mdi:close-octagon-outline", - ], -} + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=CURRENT_WARNING_SENSOR, + name="Current Warning Level", + icon="mdi:close-octagon-outline", + ), + SensorEntityDescription( + key=ADVANCE_WARNING_SENSOR, + name="Advance Warning Level", + icon="mdi:close-octagon-outline", + ), +) +MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -79,9 +88,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): api = WrappedDwDWWAPI(DwdWeatherWarningsAPI(region_name)) - sensors = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - sensors.append(DwdWeatherWarningsSensor(api, name, sensor_type)) + sensors = [ + DwdWeatherWarningsSensor(api, name, description) + for description in SENSOR_TYPES + if description.key in config[CONF_MONITORED_CONDITIONS] + ] add_entities(sensors, True) @@ -89,31 +100,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DwdWeatherWarningsSensor(SensorEntity): """Representation of a DWD-Weather-Warnings sensor.""" - def __init__(self, api, name, sensor_type): + def __init__( + self, + api, + name, + description: SensorEntityDescription, + ): """Initialize a DWD-Weather-Warnings sensor.""" self._api = api - self._name = name - self._sensor_type = sensor_type - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {MONITORED_CONDITIONS[self._sensor_type][0]}" - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return MONITORED_CONDITIONS[self._sensor_type][2] - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return MONITORED_CONDITIONS[self._sensor_type][1] + self.entity_description = description + self._attr_name = f"{name} {description.name}" @property def state(self): """Return the state of the device.""" - if self._sensor_type == CURRENT_WARNING_SENSOR: + if self.entity_description.key == CURRENT_WARNING_SENSOR: return self._api.api.current_warning_level return self._api.api.expected_warning_level @@ -127,7 +128,7 @@ class DwdWeatherWarningsSensor(SensorEntity): ATTR_LAST_UPDATE: self._api.api.last_update, } - if self._sensor_type == CURRENT_WARNING_SENSOR: + if self.entity_description.key == CURRENT_WARNING_SENSOR: searched_warnings = self._api.api.current_warnings else: searched_warnings = self._api.api.expected_warnings @@ -165,7 +166,7 @@ class DwdWeatherWarningsSensor(SensorEntity): "Update requested for %s (%s) by %s", self._api.api.warncell_name, self._api.api.warncell_id, - self._sensor_type, + self.entity_description.key, ) self._api.update() From 3908aabc13f78caa0d855a80d50243b005648bb3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Jul 2021 23:47:29 +0200 Subject: [PATCH 667/818] Use EntityDescription - climacell (#53573) * Use EntityDescription - climacell * Fix tests * Fix coverage ignore comment --- .../components/climacell/__init__.py | 4 +- homeassistant/components/climacell/const.py | 246 ++++++++++-------- homeassistant/components/climacell/sensor.py | 47 ++-- tests/components/climacell/test_const.py | 8 +- 4 files changed, 175 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 9fd3e0b8340..97090cef31d 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -222,7 +222,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_PRECIPITATION_TYPE, - *(sensor_type.field for sensor_type in CC_V3_SENSOR_TYPES), + *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), ] ) data[FORECASTS][HOURLY] = await self._api.forecast_hourly( @@ -279,7 +279,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_ATTR_WIND_GUST, CC_ATTR_CLOUD_COVER, CC_ATTR_PRECIPITATION_TYPE, - *(sensor_type.field for sensor_type in CC_SENSOR_TYPES), + *(sensor_type.key for sensor_type in CC_SENSOR_TYPES), ], [ CC_ATTR_TEMPERATURE_LOW, diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index c11c0b1774b..bcbd139028a 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -17,6 +17,7 @@ from pyclimacell.const import ( WeatherCode, ) +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -151,11 +152,9 @@ CC_ATTR_CLOUD_CEILING = "cloudCeiling" @dataclass -class ClimaCellSensorMetadata: - """Metadata about an individual ClimaCell sensor.""" +class ClimaCellSensorEntityDescription(SensorEntityDescription): + """Describes a ClimaCell sensor entity.""" - field: str - name: str unit_imperial: str | None = None unit_metric: str | None = None metric_conversion: Callable[[float], float] | float = 1.0 @@ -171,30 +170,33 @@ class ClimaCellSensorMetadata: "`unit_imperial` and `unit_metric` both need to be None or both need " "to be defined." ) + if self.name is None: # pragma: no cover + raise TypeError + self.name_ = self.name CC_SENSOR_TYPES = ( - ClimaCellSensorMetadata( - CC_ATTR_FEELS_LIKE, - "Feels Like", + ClimaCellSensorEntityDescription( + key=CC_ATTR_FEELS_LIKE, + name="Feels Like", unit_imperial=TEMP_FAHRENHEIT, unit_metric=TEMP_CELSIUS, metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), is_metric_check=True, device_class=DEVICE_CLASS_TEMPERATURE, ), - ClimaCellSensorMetadata( - CC_ATTR_DEW_POINT, - "Dew Point", + ClimaCellSensorEntityDescription( + key=CC_ATTR_DEW_POINT, + name="Dew Point", unit_imperial=TEMP_FAHRENHEIT, unit_metric=TEMP_CELSIUS, metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), is_metric_check=True, device_class=DEVICE_CLASS_TEMPERATURE, ), - ClimaCellSensorMetadata( - CC_ATTR_PRESSURE_SURFACE_LEVEL, - "Pressure (Surface Level)", + ClimaCellSensorEntityDescription( + key=CC_ATTR_PRESSURE_SURFACE_LEVEL, + name="Pressure (Surface Level)", unit_imperial=PRESSURE_INHG, unit_metric=PRESSURE_HPA, metric_conversion=lambda val: pressure_convert( @@ -203,17 +205,17 @@ CC_SENSOR_TYPES = ( is_metric_check=True, device_class=DEVICE_CLASS_PRESSURE, ), - ClimaCellSensorMetadata( - CC_ATTR_SOLAR_GHI, - "Global Horizontal Irradiance", + ClimaCellSensorEntityDescription( + key=CC_ATTR_SOLAR_GHI, + name="Global Horizontal Irradiance", unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, metric_conversion=3.15459, is_metric_check=True, ), - ClimaCellSensorMetadata( - CC_ATTR_CLOUD_BASE, - "Cloud Base", + ClimaCellSensorEntityDescription( + key=CC_ATTR_CLOUD_BASE, + name="Cloud Base", unit_imperial=LENGTH_MILES, unit_metric=LENGTH_KILOMETERS, metric_conversion=lambda val: distance_convert( @@ -221,9 +223,9 @@ CC_SENSOR_TYPES = ( ), is_metric_check=True, ), - ClimaCellSensorMetadata( - CC_ATTR_CLOUD_CEILING, - "Cloud Ceiling", + ClimaCellSensorEntityDescription( + key=CC_ATTR_CLOUD_CEILING, + name="Cloud Ceiling", unit_imperial=LENGTH_MILES, unit_metric=LENGTH_KILOMETERS, metric_conversion=lambda val: distance_convert( @@ -231,99 +233,114 @@ CC_SENSOR_TYPES = ( ), is_metric_check=True, ), - ClimaCellSensorMetadata( - CC_ATTR_CLOUD_COVER, - "Cloud Cover", + ClimaCellSensorEntityDescription( + key=CC_ATTR_CLOUD_COVER, + name="Cloud Cover", unit_imperial=PERCENTAGE, unit_metric=PERCENTAGE, ), - ClimaCellSensorMetadata( - CC_ATTR_WIND_GUST, - "Wind Gust", + ClimaCellSensorEntityDescription( + key=CC_ATTR_WIND_GUST, + name="Wind Gust", unit_imperial=SPEED_MILES_PER_HOUR, unit_metric=SPEED_METERS_PER_SECOND, metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) / 3600, is_metric_check=True, ), - ClimaCellSensorMetadata( - CC_ATTR_PRECIPITATION_TYPE, - "Precipitation Type", + ClimaCellSensorEntityDescription( + key=CC_ATTR_PRECIPITATION_TYPE, + name="Precipitation Type", value_map=PrecipitationType, ), - ClimaCellSensorMetadata( - CC_ATTR_OZONE, - "Ozone", + ClimaCellSensorEntityDescription( + key=CC_ATTR_OZONE, + name="Ozone", unit_imperial=CONCENTRATION_PARTS_PER_BILLION, unit_metric=CONCENTRATION_PARTS_PER_BILLION, ), - ClimaCellSensorMetadata( - CC_ATTR_PARTICULATE_MATTER_25, - "Particulate Matter < 2.5 μm", + ClimaCellSensorEntityDescription( + key=CC_ATTR_PARTICULATE_MATTER_25, + name="Particulate Matter < 2.5 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, metric_conversion=3.2808399 ** 3, is_metric_check=True, ), - ClimaCellSensorMetadata( - CC_ATTR_PARTICULATE_MATTER_10, - "Particulate Matter < 10 μm", + ClimaCellSensorEntityDescription( + key=CC_ATTR_PARTICULATE_MATTER_10, + name="Particulate Matter < 10 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, metric_conversion=3.2808399 ** 3, is_metric_check=True, ), - ClimaCellSensorMetadata( - CC_ATTR_NITROGEN_DIOXIDE, - "Nitrogen Dioxide", + ClimaCellSensorEntityDescription( + key=CC_ATTR_NITROGEN_DIOXIDE, + name="Nitrogen Dioxide", unit_imperial=CONCENTRATION_PARTS_PER_BILLION, unit_metric=CONCENTRATION_PARTS_PER_BILLION, ), - ClimaCellSensorMetadata( - CC_ATTR_CARBON_MONOXIDE, - "Carbon Monoxide", + ClimaCellSensorEntityDescription( + key=CC_ATTR_CARBON_MONOXIDE, + name="Carbon Monoxide", unit_imperial=CONCENTRATION_PARTS_PER_MILLION, unit_metric=CONCENTRATION_PARTS_PER_MILLION, device_class=DEVICE_CLASS_CO, ), - ClimaCellSensorMetadata( - CC_ATTR_SULFUR_DIOXIDE, - "Sulfur Dioxide", + ClimaCellSensorEntityDescription( + key=CC_ATTR_SULFUR_DIOXIDE, + name="Sulfur Dioxide", unit_imperial=CONCENTRATION_PARTS_PER_BILLION, unit_metric=CONCENTRATION_PARTS_PER_BILLION, ), - ClimaCellSensorMetadata(CC_ATTR_EPA_AQI, "US EPA Air Quality Index"), - ClimaCellSensorMetadata( - CC_ATTR_EPA_PRIMARY_POLLUTANT, - "US EPA Primary Pollutant", + ClimaCellSensorEntityDescription( + key=CC_ATTR_EPA_AQI, + name="US EPA Air Quality Index", + ), + ClimaCellSensorEntityDescription( + key=CC_ATTR_EPA_PRIMARY_POLLUTANT, + name="US EPA Primary Pollutant", value_map=PrimaryPollutantType, ), - ClimaCellSensorMetadata( - CC_ATTR_EPA_HEALTH_CONCERN, - "US EPA Health Concern", + ClimaCellSensorEntityDescription( + key=CC_ATTR_EPA_HEALTH_CONCERN, + name="US EPA Health Concern", value_map=HealthConcernType, ), - ClimaCellSensorMetadata(CC_ATTR_CHINA_AQI, "China MEP Air Quality Index"), - ClimaCellSensorMetadata( - CC_ATTR_CHINA_PRIMARY_POLLUTANT, - "China MEP Primary Pollutant", + ClimaCellSensorEntityDescription( + key=CC_ATTR_CHINA_AQI, + name="China MEP Air Quality Index", + ), + ClimaCellSensorEntityDescription( + key=CC_ATTR_CHINA_PRIMARY_POLLUTANT, + name="China MEP Primary Pollutant", value_map=PrimaryPollutantType, ), - ClimaCellSensorMetadata( - CC_ATTR_CHINA_HEALTH_CONCERN, - "China MEP Health Concern", + ClimaCellSensorEntityDescription( + key=CC_ATTR_CHINA_HEALTH_CONCERN, + name="China MEP Health Concern", value_map=HealthConcernType, ), - ClimaCellSensorMetadata( - CC_ATTR_POLLEN_TREE, "Tree Pollen Index", value_map=PollenIndex + ClimaCellSensorEntityDescription( + key=CC_ATTR_POLLEN_TREE, + name="Tree Pollen Index", + value_map=PollenIndex, ), - ClimaCellSensorMetadata( - CC_ATTR_POLLEN_WEED, "Weed Pollen Index", value_map=PollenIndex + ClimaCellSensorEntityDescription( + key=CC_ATTR_POLLEN_WEED, + name="Weed Pollen Index", + value_map=PollenIndex, ), - ClimaCellSensorMetadata( - CC_ATTR_POLLEN_GRASS, "Grass Pollen Index", value_map=PollenIndex + ClimaCellSensorEntityDescription( + key=CC_ATTR_POLLEN_GRASS, + name="Grass Pollen Index", + value_map=PollenIndex, + ), + ClimaCellSensorEntityDescription( + CC_ATTR_FIRE_INDEX, + name="Fire Index", ), - ClimaCellSensorMetadata(CC_ATTR_FIRE_INDEX, "Fire Index"), ) # V3 constants @@ -389,67 +406,88 @@ CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" CC_V3_ATTR_FIRE_INDEX = "fire_index" CC_V3_SENSOR_TYPES = ( - ClimaCellSensorMetadata( - CC_V3_ATTR_OZONE, - "Ozone", + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_OZONE, + name="Ozone", unit_imperial=CONCENTRATION_PARTS_PER_BILLION, unit_metric=CONCENTRATION_PARTS_PER_BILLION, ), - ClimaCellSensorMetadata( - CC_V3_ATTR_PARTICULATE_MATTER_25, - "Particulate Matter < 2.5 μm", + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_PARTICULATE_MATTER_25, + name="Particulate Matter < 2.5 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, metric_conversion=3.2808399 ** 3, is_metric_check=False, ), - ClimaCellSensorMetadata( - CC_V3_ATTR_PARTICULATE_MATTER_10, - "Particulate Matter < 10 μm", + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_PARTICULATE_MATTER_10, + name="Particulate Matter < 10 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, metric_conversion=3.2808399 ** 3, is_metric_check=False, ), - ClimaCellSensorMetadata( - CC_V3_ATTR_NITROGEN_DIOXIDE, - "Nitrogen Dioxide", + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_NITROGEN_DIOXIDE, + name="Nitrogen Dioxide", unit_imperial=CONCENTRATION_PARTS_PER_BILLION, unit_metric=CONCENTRATION_PARTS_PER_BILLION, ), - ClimaCellSensorMetadata( - CC_V3_ATTR_CARBON_MONOXIDE, - "Carbon Monoxide", + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_CARBON_MONOXIDE, + name="Carbon Monoxide", unit_imperial=CONCENTRATION_PARTS_PER_MILLION, unit_metric=CONCENTRATION_PARTS_PER_MILLION, device_class=DEVICE_CLASS_CO, ), - ClimaCellSensorMetadata( - CC_V3_ATTR_SULFUR_DIOXIDE, - "Sulfur Dioxide", + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_SULFUR_DIOXIDE, + name="Sulfur Dioxide", unit_imperial=CONCENTRATION_PARTS_PER_BILLION, unit_metric=CONCENTRATION_PARTS_PER_BILLION, ), - ClimaCellSensorMetadata(CC_V3_ATTR_EPA_AQI, "US EPA Air Quality Index"), - ClimaCellSensorMetadata( - CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, "US EPA Primary Pollutant" + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_EPA_AQI, + name="US EPA Air Quality Index", ), - ClimaCellSensorMetadata(CC_V3_ATTR_EPA_HEALTH_CONCERN, "US EPA Health Concern"), - ClimaCellSensorMetadata(CC_V3_ATTR_CHINA_AQI, "China MEP Air Quality Index"), - ClimaCellSensorMetadata( - CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, "China MEP Primary Pollutant" + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, + name="US EPA Primary Pollutant", ), - ClimaCellSensorMetadata( - CC_V3_ATTR_CHINA_HEALTH_CONCERN, "China MEP Health Concern" + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_EPA_HEALTH_CONCERN, + name="US EPA Health Concern", ), - ClimaCellSensorMetadata( - CC_V3_ATTR_POLLEN_TREE, "Tree Pollen Index", value_map=V3PollenIndex + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_CHINA_AQI, + name="China MEP Air Quality Index", ), - ClimaCellSensorMetadata( - CC_V3_ATTR_POLLEN_WEED, "Weed Pollen Index", value_map=V3PollenIndex + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, + name="China MEP Primary Pollutant", ), - ClimaCellSensorMetadata( - CC_V3_ATTR_POLLEN_GRASS, "Grass Pollen Index", value_map=V3PollenIndex + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_CHINA_HEALTH_CONCERN, + name="China MEP Health Concern", + ), + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_POLLEN_TREE, + name="Tree Pollen Index", + value_map=V3PollenIndex, + ), + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_POLLEN_WEED, + name="Weed Pollen Index", + value_map=V3PollenIndex, + ), + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_POLLEN_GRASS, + name="Grass Pollen Index", + value_map=V3PollenIndex, + ), + ClimaCellSensorEntityDescription( + key=CC_V3_ATTR_FIRE_INDEX, + name="Fire Index", ), - ClimaCellSensorMetadata(CC_V3_ATTR_FIRE_INDEX, "Fire Index"), ) diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 77fa6486013..d67cde18e0b 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -14,7 +14,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import CC_SENSOR_TYPES, CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorMetadata +from .const import ( + CC_SENSOR_TYPES, + CC_V3_SENSOR_TYPES, + DOMAIN, + ClimaCellSensorEntityDescription, +) _LOGGER = logging.getLogger(__name__) @@ -35,8 +40,8 @@ async def async_setup_entry( api_class = ClimaCellSensorEntity sensor_types = CC_SENSOR_TYPES entities = [ - api_class(hass, config_entry, coordinator, api_version, sensor_type) - for sensor_type in sensor_types + api_class(hass, config_entry, coordinator, api_version, description) + for description in sensor_types ] async_add_entities(entities) @@ -44,30 +49,29 @@ async def async_setup_entry( class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): """Base ClimaCell sensor entity.""" + entity_description: ClimaCellSensorEntityDescription + def __init__( self, hass: HomeAssistant, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator, api_version: int, - sensor_type: ClimaCellSensorMetadata, + description: ClimaCellSensorEntityDescription, ) -> None: """Initialize ClimaCell Sensor Entity.""" super().__init__(config_entry, coordinator, api_version) - self.sensor_type = sensor_type - self._attr_device_class = self.sensor_type.device_class + self.entity_description = description self._attr_entity_registry_enabled_default = False - self._attr_name = ( - f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type.name}" - ) + self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" self._attr_unique_id = ( - f"{self._config_entry.unique_id}_{slugify(self.sensor_type.name)}" + f"{self._config_entry.unique_id}_{slugify(description.name_)}" ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} self._attr_unit_of_measurement = ( - self.sensor_type.unit_metric + description.unit_metric if hass.config.units.is_metric - else self.sensor_type.unit_imperial + else description.unit_imperial ) @property @@ -81,20 +85,21 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): state = self._state if ( state is not None - and self.sensor_type.unit_imperial is not None - and self.sensor_type.metric_conversion != 1.0 - and self.sensor_type.is_metric_check is not None - and self.hass.config.units.is_metric == self.sensor_type.is_metric_check + and self.entity_description.unit_imperial is not None + and self.entity_description.metric_conversion != 1.0 + and self.entity_description.is_metric_check is not None + and self.hass.config.units.is_metric + == self.entity_description.is_metric_check ): - conversion = self.sensor_type.metric_conversion + conversion = self.entity_description.metric_conversion # When conversion is a callable, we assume it's a single input function if callable(conversion): return round(conversion(state), 4) return round(state * conversion, 4) - if self.sensor_type.value_map is not None and state is not None: - return self.sensor_type.value_map(state).name.lower() + if self.entity_description.value_map is not None and state is not None: + return self.entity_description.value_map(state).name.lower() return state @@ -105,7 +110,7 @@ class ClimaCellSensorEntity(BaseClimaCellSensorEntity): @property def _state(self) -> str | int | float | None: """Return the raw state.""" - return self._get_current_property(self.sensor_type.field) + return self._get_current_property(self.entity_description.key) class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): @@ -115,5 +120,5 @@ class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): def _state(self) -> str | int | float | None: """Return the raw state.""" return self._get_cc_value( - self.coordinator.data[CURRENT], self.sensor_type.field + self.coordinator.data[CURRENT], self.entity_description.key ) diff --git a/tests/components/climacell/test_const.py b/tests/components/climacell/test_const.py index ba8eb6d8f39..2719426a7a0 100644 --- a/tests/components/climacell/test_const.py +++ b/tests/components/climacell/test_const.py @@ -1,12 +1,14 @@ """Tests for ClimaCell const.""" import pytest -from homeassistant.components.climacell.const import ClimaCellSensorMetadata +from homeassistant.components.climacell.const import ClimaCellSensorEntityDescription from homeassistant.const import TEMP_FAHRENHEIT async def test_post_init(): - """Test post initiailization check for ClimaCellSensorMetadata.""" + """Test post initiailization check for ClimaCellSensorEntityDescription.""" with pytest.raises(RuntimeError): - ClimaCellSensorMetadata("a", "b", unit_imperial=TEMP_FAHRENHEIT) + ClimaCellSensorEntityDescription( + key="a", name="b", unit_imperial=TEMP_FAHRENHEIT + ) From 14c257e1b76d6bffce99ae1e79e17cfcd12b71b5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 00:07:14 +0200 Subject: [PATCH 668/818] Use EntityDescription - melcloud (#53572) * Use EntityDescription - melcloud * Fix pylint errors * Fix test * Fix coverage exclude comments --- homeassistant/components/melcloud/sensor.py | 191 ++++++++++-------- .../melcloud/test_atw_zone_sensor.py | 6 +- 2 files changed, 107 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index b0f1d73f238..104f463306f 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -1,7 +1,8 @@ """Support for MelCloud device sensors.""" from __future__ import annotations -from typing import Any, Callable, NamedTuple +from dataclasses import dataclass +from typing import Any, Callable from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW from pymelcloud.atw_device import Zone @@ -11,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS from homeassistant.util import dt as dt_util @@ -19,130 +21,146 @@ from . import MelCloudDevice from .const import DOMAIN -class SensorMetadata(NamedTuple): - """Metadata for an individual sensor.""" +@dataclass +class MelcloudSensorEntityDescription(SensorEntityDescription): + """Describes Melcloud sensor entity.""" - measurement_name: str - icon: str - unit: str - device_class: str - value_fn: Callable[[Any], float] - enabled: Callable[[Any], bool] + _value_fn: Callable[[Any], float] | None = None + _enabled: Callable[[Any], bool] | None = None + + def __post_init__(self) -> None: + """Ensure all required fields are set.""" + if self._value_fn is None: # pragma: no cover + raise TypeError + if self._enabled is None: # pragma: no cover + raise TypeError + self.value_fn = self._value_fn + self.enabled = self._enabled -ATA_SENSORS: dict[str, SensorMetadata] = { - "room_temperature": SensorMetadata( - "Room Temperature", +ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( + MelcloudSensorEntityDescription( + key="room_temperature", + name="Room Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda x: x.device.room_temperature, - enabled=lambda x: True, + _value_fn=lambda x: x.device.room_temperature, + _enabled=lambda x: True, ), - "energy": SensorMetadata( - "Energy", + MelcloudSensorEntityDescription( + key="energy", + name="Energy", icon="mdi:factory", - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - value_fn=lambda x: x.device.total_energy_consumed, - enabled=lambda x: x.device.has_energy_consumed_meter, + _value_fn=lambda x: x.device.total_energy_consumed, + _enabled=lambda x: x.device.has_energy_consumed_meter, ), -} -ATW_SENSORS: dict[str, SensorMetadata] = { - "outside_temperature": SensorMetadata( - "Outside Temperature", +) +ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( + MelcloudSensorEntityDescription( + key="outside_temperature", + name="Outside Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda x: x.device.outside_temperature, - enabled=lambda x: True, + _value_fn=lambda x: x.device.outside_temperature, + _enabled=lambda x: True, ), - "tank_temperature": SensorMetadata( - "Tank Temperature", + MelcloudSensorEntityDescription( + key="tank_temperature", + name="Tank Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda x: x.device.tank_temperature, - enabled=lambda x: True, + _value_fn=lambda x: x.device.tank_temperature, + _enabled=lambda x: True, ), -} -ATW_ZONE_SENSORS: dict[str, SensorMetadata] = { - "room_temperature": SensorMetadata( - "Room Temperature", +) +ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( + MelcloudSensorEntityDescription( + key="room_temperature", + name="Room Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda zone: zone.room_temperature, - enabled=lambda x: True, + _value_fn=lambda zone: zone.room_temperature, + _enabled=lambda x: True, ), - "flow_temperature": SensorMetadata( - "Flow Temperature", + MelcloudSensorEntityDescription( + key="flow_temperature", + name="Flow Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda zone: zone.flow_temperature, - enabled=lambda x: True, + _value_fn=lambda zone: zone.flow_temperature, + _enabled=lambda x: True, ), - "return_temperature": SensorMetadata( - "Flow Return Temperature", + MelcloudSensorEntityDescription( + key="return_temperature", + name="Flow Return Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda zone: zone.return_temperature, - enabled=lambda x: True, + _value_fn=lambda zone: zone.return_temperature, + _enabled=lambda x: True, ), -} +) async def async_setup_entry(hass, entry, async_add_entities): """Set up MELCloud device sensors based on config_entry.""" mel_devices = hass.data[DOMAIN].get(entry.entry_id) - async_add_entities( + + entities: list[MelDeviceSensor] = [ + MelDeviceSensor(mel_device, description) + for description in ATA_SENSORS + for mel_device in mel_devices[DEVICE_TYPE_ATA] + if description.enabled(mel_device) + ] + [ + MelDeviceSensor(mel_device, description) + for description in ATW_SENSORS + for mel_device in mel_devices[DEVICE_TYPE_ATW] + if description.enabled(mel_device) + ] + entities.extend( [ - MelDeviceSensor(mel_device, measurement, metadata) - for measurement, metadata in ATA_SENSORS.items() - for mel_device in mel_devices[DEVICE_TYPE_ATA] - if metadata.enabled(mel_device) - ] - + [ - MelDeviceSensor(mel_device, measurement, metadata) - for measurement, metadata in ATW_SENSORS.items() - for mel_device in mel_devices[DEVICE_TYPE_ATW] - if metadata.enabled(mel_device) - ] - + [ - AtwZoneSensor(mel_device, zone, measurement, metadata) + AtwZoneSensor(mel_device, zone, description) for mel_device in mel_devices[DEVICE_TYPE_ATW] for zone in mel_device.device.zones - for measurement, metadata, in ATW_ZONE_SENSORS.items() - if metadata.enabled(zone) - ], - True, + for description in ATW_ZONE_SENSORS + if description.enabled(zone) + ] ) + async_add_entities(entities, True) class MelDeviceSensor(SensorEntity): """Representation of a Sensor.""" - def __init__(self, api: MelCloudDevice, measurement, metadata: SensorMetadata): + entity_description: MelcloudSensorEntityDescription + + def __init__( + self, + api: MelCloudDevice, + description: MelcloudSensorEntityDescription, + ) -> None: """Initialize the sensor.""" self._api = api - self._metadata = metadata + self.entity_description = description - self._attr_device_class = metadata.device_class - self._attr_icon = metadata.icon - self._attr_name = f"{api.name} {metadata.measurement_name}" - self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{measurement}" - self._attr_unit_of_measurement = metadata.unit + self._attr_name = f"{api.name} {description.name}" + self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{description.key}" self._attr_state_class = STATE_CLASS_MEASUREMENT - if metadata.device_class == DEVICE_CLASS_ENERGY: + if description.device_class == DEVICE_CLASS_ENERGY: self._attr_last_reset = dt_util.utc_from_timestamp(0) @property def state(self): """Return the state of the sensor.""" - return self._metadata.value_fn(self._api) + return self.entity_description.value_fn(self._api) async def async_update(self): """Retrieve latest state.""" @@ -158,18 +176,19 @@ class AtwZoneSensor(MelDeviceSensor): """Air-to-Air device sensor.""" def __init__( - self, api: MelCloudDevice, zone: Zone, measurement, metadata: SensorMetadata - ): + self, + api: MelCloudDevice, + zone: Zone, + description: MelcloudSensorEntityDescription, + ) -> None: """Initialize the sensor.""" - if zone.zone_index == 1: - full_measurement = measurement - else: - full_measurement = f"{measurement}-zone-{zone.zone_index}" - super().__init__(api, full_measurement, metadata) + if zone.zone_index != 1: + description.key = f"{description.key}-zone-{zone.zone_index}" + super().__init__(api, description) self._zone = zone - self._attr_name = f"{api.name} {zone.name} {metadata.measurement_name}" + self._attr_name = f"{api.name} {zone.name} {description.name}" @property def state(self): """Return zone based state.""" - return self._metadata.value_fn(self._zone) + return self.entity_description.value_fn(self._zone) diff --git a/tests/components/melcloud/test_atw_zone_sensor.py b/tests/components/melcloud/test_atw_zone_sensor.py index 6e6487a3774..5938f1af1f1 100644 --- a/tests/components/melcloud/test_atw_zone_sensor.py +++ b/tests/components/melcloud/test_atw_zone_sensor.py @@ -37,15 +37,13 @@ def test_zone_unique_ids(mock_device, mock_zone_1, mock_zone_2): sensor_1 = AtwZoneSensor( mock_device, mock_zone_1, - "room_temperature", - ATW_ZONE_SENSORS["room_temperature"], + ATW_ZONE_SENSORS[0], # room_temperature ) assert sensor_1.unique_id == "1234-11:11:11:11:11:11-room_temperature" sensor_2 = AtwZoneSensor( mock_device, mock_zone_2, - "room_temperature", - ATW_ZONE_SENSORS["flow_temperature"], + ATW_ZONE_SENSORS[0], # room_temperature ) assert sensor_2.unique_id == "1234-11:11:11:11:11:11-room_temperature-zone-2" From 73bc0267e9b40ef170080011e2a31b454f888f5d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 27 Jul 2021 18:55:55 -0400 Subject: [PATCH 669/818] Add DeviceRegistry template functions (#53131) --- homeassistant/helpers/template.py | 57 +++++++++++- tests/helpers/test_template.py | 145 ++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d991a0b58f2..66354aa7aa6 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -43,7 +43,11 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import entity_registry, location as loc_helper +from homeassistant.helpers import ( + device_registry, + entity_registry, + location as loc_helper, +) from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util @@ -902,13 +906,49 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: return sorted(found.values(), key=lambda a: a.entity_id) -def device_entities(hass: HomeAssistant, device_id: str) -> Iterable[str]: +def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]: """Get entity ids for entities tied to a device.""" entity_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_device(entity_reg, device_id) + entries = entity_registry.async_entries_for_device(entity_reg, _device_id) return [entry.entity_id for entry in entries] +def device_id(hass: HomeAssistant, entity_id: str) -> str | None: + """Get a device ID from an entity ID.""" + if not isinstance(entity_id, str) or "." not in entity_id: + raise TemplateError(f"Must provide an entity ID, got {entity_id}") # type: ignore + entity_reg = entity_registry.async_get(hass) + entity = entity_reg.async_get(entity_id) + if entity is None: + return None + return entity.device_id + + +def device_attr(hass: HomeAssistant, device_or_entity_id: str, attr_name: str) -> Any: + """Get the device specific attribute.""" + device_reg = device_registry.async_get(hass) + if not isinstance(device_or_entity_id, str): + raise TemplateError("Must provide a device or entity ID") + device = None + if ( + "." in device_or_entity_id + and (_device_id := device_id(hass, device_or_entity_id)) is not None + ): + device = device_reg.async_get(_device_id) + elif "." not in device_or_entity_id: + device = device_reg.async_get(device_or_entity_id) + if device is None or not hasattr(device, attr_name): + return None + return getattr(device, attr_name) + + +def is_device_attr( + hass: HomeAssistant, device_or_entity_id: str, attr_name: str, attr_value: Any +) -> bool: + """Test if a device's attribute is a specific value.""" + return bool(device_attr(hass, device_or_entity_id, attr_name) == attr_value) + + def closest(hass, *args): """Find closest entity. @@ -1486,6 +1526,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["device_entities"] = hassfunction(device_entities) self.filters["device_entities"] = pass_context(self.globals["device_entities"]) + self.globals["device_attr"] = hassfunction(device_attr) + self.globals["is_device_attr"] = hassfunction(is_device_attr) + + self.globals["device_id"] = hassfunction(device_id) + self.filters["device_id"] = pass_context(self.globals["device_id"]) + if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. @@ -1507,8 +1553,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "states", "utcnow", "now", + "device_attr", + "is_device_attr", + "device_id", ] - hass_filters = ["closest", "expand"] + hass_filters = ["closest", "expand", "device_id"] for glob in hass_globals: self.globals[glob] = unsupported(glob) for filt in hass_filters: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 2547537bff9..d6fe2b6dbaf 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1585,6 +1585,151 @@ async def test_device_entities(hass): assert info.rate_limit is None +async def test_device_id(hass): + """Test device_id function.""" + config_entry = MockConfigEntry(domain="light") + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="test", + ) + entity_entry = entity_registry.async_get_or_create( + "sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id + ) + entity_entry_no_device = entity_registry.async_get_or_create( + "sensor", "test", "test_no_device", suggested_object_id="test" + ) + + info = render_to_info(hass, "{{ 'sensor.fail' | device_id }}") + assert_result_info(info, None) + assert info.rate_limit is None + + with pytest.raises(TemplateError): + info = render_to_info(hass, "{{ 56 | device_id }}") + assert_result_info(info, None) + + with pytest.raises(TemplateError): + info = render_to_info(hass, "{{ 'not_a_real_entity_id' | device_id }}") + assert_result_info(info, None) + + info = render_to_info( + hass, f"{{{{ device_id('{entity_entry_no_device.entity_id}') }}}}" + ) + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ device_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, device_entry.id) + assert info.rate_limit is None + + +async def test_device_attr(hass): + """Test device_attr and is_device_attr functions.""" + config_entry = MockConfigEntry(domain="light") + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + # Test non existing device ids (device_attr) + info = render_to_info(hass, "{{ device_attr('abc123', 'id') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + with pytest.raises(TemplateError): + info = render_to_info(hass, "{{ device_attr(56, 'id') }}") + assert_result_info(info, None) + + # Test non existing device ids (is_device_attr) + info = render_to_info(hass, "{{ is_device_attr('abc123', 'id', 'test') }}") + assert_result_info(info, False) + assert info.rate_limit is None + + with pytest.raises(TemplateError): + info = render_to_info(hass, "{{ is_device_attr(56, 'id', 'test') }}") + assert_result_info(info, False) + + # Test non existing entity id (device_attr) + info = render_to_info(hass, "{{ device_attr('entity.test', 'id') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing entity id (is_device_attr) + info = render_to_info(hass, "{{ is_device_attr('entity.test', 'id', 'test') }}") + assert_result_info(info, False) + assert info.rate_limit is None + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="test", + ) + entity_entry = entity_registry.async_get_or_create( + "sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id + ) + + # Test non existent device attribute (device_attr) + info = render_to_info( + hass, f"{{{{ device_attr('{device_entry.id}', 'invalid_attr') }}}}" + ) + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existent device attribute (is_device_attr) + info = render_to_info( + hass, f"{{{{ is_device_attr('{device_entry.id}', 'invalid_attr', 'test') }}}}" + ) + assert_result_info(info, False) + assert info.rate_limit is None + + # Test None device attribute (device_attr) + info = render_to_info( + hass, f"{{{{ device_attr('{device_entry.id}', 'manufacturer') }}}}" + ) + assert_result_info(info, None) + assert info.rate_limit is None + + # Test None device attribute mismatch (is_device_attr) + info = render_to_info( + hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', 'test') }}}}" + ) + assert_result_info(info, False) + assert info.rate_limit is None + + # Test None device attribute match (is_device_attr) + info = render_to_info( + hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', None) }}}}" + ) + assert_result_info(info, True) + assert info.rate_limit is None + + # Test valid device attribute match (device_attr) + info = render_to_info(hass, f"{{{{ device_attr('{device_entry.id}', 'model') }}}}") + assert_result_info(info, "test") + assert info.rate_limit is None + + # Test valid device attribute match (device_attr) + info = render_to_info( + hass, f"{{{{ device_attr('{entity_entry.entity_id}', 'model') }}}}" + ) + assert_result_info(info, "test") + assert info.rate_limit is None + + # Test valid device attribute mismatch (is_device_attr) + info = render_to_info( + hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'fail') }}}}" + ) + assert_result_info(info, False) + assert info.rate_limit is None + + # Test valid device attribute match (is_device_attr) + info = render_to_info( + hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'test') }}}}" + ) + assert_result_info(info, True) + assert info.rate_limit is None + + def test_closest_function_to_coord(hass): """Test closest function to coord.""" hass.states.async_set( From a004a0dd4fa01cff14299bce17bcc31360965a5a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 27 Jul 2021 16:48:04 -0700 Subject: [PATCH 670/818] Bump frontend to 20210727.0 (#53591) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 83e6d71712a..470dae0b32e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210726.0" + "home-assistant-frontend==20210727.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 37cfbe539aa..20dbd41e89b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210726.0 +home-assistant-frontend==20210727.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 7fd36f3b516..320a4377d0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -785,7 +785,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210726.0 +home-assistant-frontend==20210727.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c87c74966ac..98dc1f34e9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -452,7 +452,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210726.0 +home-assistant-frontend==20210727.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 127c9fc8770d31de04101339813ed8d09a1a8dd4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 Jul 2021 01:48:22 +0200 Subject: [PATCH 671/818] Add statistics support for SMA energy sensors (#53589) --- homeassistant/components/sma/sensor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 3894f864ffb..6f3f7c2dca9 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -7,7 +7,11 @@ from typing import Any import pysma import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -16,6 +20,8 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -26,6 +32,7 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util import dt as dt_util from .const import ( CONF_CUSTOM, @@ -157,6 +164,11 @@ class SMAsensor(CoordinatorEntity, SensorEntity): self._config_entry_unique_id = config_entry_unique_id self._device_info = device_info + if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: + self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_last_reset = dt_util.utc_from_timestamp(0) + # Set sensor enabled to False. # Will be enabled by async_added_to_hass if actually used. self._sensor.enabled = False From 85c1614204ff60639c7149cdc1e8f57bb67ff90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 28 Jul 2021 06:05:16 +0200 Subject: [PATCH 672/818] Add currency to location data (#53575) --- homeassistant/components/config/core.py | 3 +++ homeassistant/util/location.py | 9 ++++++++- tests/components/config/test_core.py | 1 + tests/components/ps4/test_config_flow.py | 1 + tests/components/ps4/test_init.py | 1 + tests/fixtures/whoami.json | 1 + tests/util/test_location.py | 19 +++++++++++++++++-- 7 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 89f4edc95d6..3b38ab49c4b 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -89,4 +89,7 @@ async def websocket_detect_config(hass, connection, msg): if location_info.time_zone: info["time_zone"] = location_info.time_zone + if location_info.currency: + info["currency"] = location_info.currency + connection.send_result(msg["id"], info) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 2a3a4ff0922..abe8fedd21a 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -12,7 +12,10 @@ from typing import Any import aiohttp +from homeassistant.const import __version__ as HA_VERSION + WHOAMI_URL = "https://whoami.home-assistant.io/v1" +WHOAMI_URL_DEV = "https://whoami-v1-dev.home-assistant.workers.dev/v1" # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -32,6 +35,7 @@ LocationInfo = collections.namedtuple( [ "ip", "country_code", + "currency", "region_code", "region_name", "city", @@ -161,7 +165,9 @@ def vincenty( async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: """Query whoami.home-assistant.io for location data.""" try: - resp = await session.get(WHOAMI_URL, timeout=30) + resp = await session.get( + WHOAMI_URL_DEV if HA_VERSION.endswith("0.dev0") else WHOAMI_URL, timeout=30 + ) except (aiohttp.ClientError, asyncio.TimeoutError): return None @@ -173,6 +179,7 @@ async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: return { "ip": raw_info.get("ip"), "country_code": raw_info.get("country"), + "currency": raw_info.get("currency"), "region_code": raw_info.get("region_code"), "region_name": raw_info.get("region"), "city": raw_info.get("city"), diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 738b1183c14..54fd6a76565 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -144,6 +144,7 @@ async def test_detect_config_fail(hass, client): return_value=location.LocationInfo( ip=None, country_code=None, + currency=None, region_code=None, region_name=None, city=None, diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 36c38a62b4c..57e65d15be4 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -64,6 +64,7 @@ MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", + "USD", "CA", "California", "San Diego", diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index f57fada8c37..8c43bd5df90 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -56,6 +56,7 @@ MOCK_CONFIG = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", + "USD", "CA", "California", "San Diego", diff --git a/tests/fixtures/whoami.json b/tests/fixtures/whoami.json index c805ef30558..f0630101483 100644 --- a/tests/fixtures/whoami.json +++ b/tests/fixtures/whoami.json @@ -3,6 +3,7 @@ "city": "Gotham", "continent": "Earth", "country": "XX", + "currency": "XXX", "latitude": "12.34567", "longitude": "12.34567", "postal_code": "12345", diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 21531a59194..d25e6859727 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,5 @@ """Test Home Assistant location util methods.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch import aiohttp import pytest @@ -76,11 +76,15 @@ async def test_detect_location_info_whoami(aioclient_mock, session): """Test detect location info using whoami.home-assistant.io.""" aioclient_mock.get(location_util.WHOAMI_URL, text=load_fixture("whoami.json")) - info = await location_util.async_detect_location_info(session, _test_real=True) + with patch("homeassistant.util.location.HA_VERSION", "1.0"): + info = await location_util.async_detect_location_info(session, _test_real=True) + + assert str(aioclient_mock.mock_calls[-1][1]) == location_util.WHOAMI_URL assert info is not None assert info.ip == "1.2.3.4" assert info.country_code == "XX" + assert info.currency == "XXX" assert info.region_code == "00" assert info.city == "Gotham" assert info.zip_code == "12345" @@ -90,6 +94,17 @@ async def test_detect_location_info_whoami(aioclient_mock, session): assert info.use_metric +async def test_dev_url(aioclient_mock, session): + """Test usage of dev URL.""" + aioclient_mock.get(location_util.WHOAMI_URL_DEV, text=load_fixture("whoami.json")) + with patch("homeassistant.util.location.HA_VERSION", "1.0.dev0"): + info = await location_util.async_detect_location_info(session, _test_real=True) + + assert str(aioclient_mock.mock_calls[-1][1]) == location_util.WHOAMI_URL_DEV + + assert info.currency == "XXX" + + async def test_whoami_query_raises(raising_session): """Test whoami query when the request to API fails.""" info = await location_util._get_whoami(raising_session) From b3b5ee10b1400094a4fa6ef132cc18152283b601 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 27 Jul 2021 22:04:59 -0700 Subject: [PATCH 673/818] Fix mypy type for timestamp validator (#53598) --- homeassistant/components/stream/worker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 1a0a7da5c39..72625fa0f5a 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -117,7 +117,6 @@ class SegmentBuffer: # Check for end of segment if packet.stream == self._input_video_stream: - if ( packet.is_keyframe and (packet.dts - self._segment_start_dts) * packet.time_base @@ -253,7 +252,7 @@ class TimestampValidator: # Number of consecutive missing decompression timestamps self._missing_dts = 0 - def is_valid(self, packet: av.Packet) -> float: + def is_valid(self, packet: av.Packet) -> bool: """Validate the packet timestamp based on ordering within the stream.""" # Discard packets missing DTS. Terminate if too many are missing. if packet.dts is None: From b7f1f2330a48a842d806d1f3a4fec8f7bf028c21 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 08:18:59 +0200 Subject: [PATCH 674/818] Use EntityDescription - netatmo (#53568) * Use EntityDescription - netatmo * Add coverage exclude * Fix coverage exclude comment --- homeassistant/components/netatmo/sensor.py | 370 ++++++++++++--------- 1 file changed, 204 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 6204e229108..58d4532e40d 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,12 +1,13 @@ """Support for the Netatmo Weather Service.""" from __future__ import annotations +from dataclasses import dataclass import logging from typing import NamedTuple, cast import pyatmo -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, @@ -48,7 +49,7 @@ from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) -SUPPORTED_PUBLIC_SENSOR_TYPES = [ +SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( "temperature", "pressure", "humidity", @@ -57,175 +58,201 @@ SUPPORTED_PUBLIC_SENSOR_TYPES = [ "guststrength", "sum_rain_1", "sum_rain_24", -] +) -class SensorMetadata(NamedTuple): - """Metadata for an individual sensor.""" +@dataclass +class NetatmoSensorEntityDescription(SensorEntityDescription): + """Describes Netatmo sensor entity.""" - name: str - netatmo_name: str - enable_default: bool - unit: str | None = None - icon: str | None = None - device_class: str | None = None + _netatmo_name: str | None = None + + def __post_init__(self) -> None: + """Ensure all required attributes are set.""" + if self._netatmo_name is None: # pragma: no cover + raise TypeError + self.netatmo_name = self._netatmo_name -SENSOR_TYPES: dict[str, SensorMetadata] = { - "temperature": SensorMetadata( - "Temperature", - netatmo_name="Temperature", - enable_default=True, - unit=TEMP_CELSIUS, +SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( + NetatmoSensorEntityDescription( + key="temperature", + name="Temperature", + _netatmo_name="Temperature", + entity_registry_enabled_default=True, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), - "temp_trend": SensorMetadata( - "Temperature trend", - netatmo_name="temp_trend", - enable_default=False, + NetatmoSensorEntityDescription( + key="temp_trend", + name="Temperature trend", + _netatmo_name="temp_trend", + entity_registry_enabled_default=False, icon="mdi:trending-up", ), - "co2": SensorMetadata( - "CO2", - netatmo_name="CO2", - unit=CONCENTRATION_PARTS_PER_MILLION, - enable_default=True, + NetatmoSensorEntityDescription( + key="co2", + name="CO2", + _netatmo_name="CO2", + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + entity_registry_enabled_default=True, device_class=DEVICE_CLASS_CO2, ), - "pressure": SensorMetadata( - "Pressure", - netatmo_name="Pressure", - enable_default=True, - unit=PRESSURE_MBAR, + NetatmoSensorEntityDescription( + key="pressure", + name="Pressure", + _netatmo_name="Pressure", + entity_registry_enabled_default=True, + unit_of_measurement=PRESSURE_MBAR, device_class=DEVICE_CLASS_PRESSURE, ), - "pressure_trend": SensorMetadata( - "Pressure trend", - netatmo_name="pressure_trend", - enable_default=False, + NetatmoSensorEntityDescription( + key="pressure_trend", + name="Pressure trend", + _netatmo_name="pressure_trend", + entity_registry_enabled_default=False, icon="mdi:trending-up", ), - "noise": SensorMetadata( - "Noise", - netatmo_name="Noise", - enable_default=True, - unit=SOUND_PRESSURE_DB, + NetatmoSensorEntityDescription( + key="noise", + name="Noise", + _netatmo_name="Noise", + entity_registry_enabled_default=True, + unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", ), - "humidity": SensorMetadata( - "Humidity", - netatmo_name="Humidity", - enable_default=True, - unit=PERCENTAGE, + NetatmoSensorEntityDescription( + key="humidity", + name="Humidity", + _netatmo_name="Humidity", + entity_registry_enabled_default=True, + unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, ), - "rain": SensorMetadata( - "Rain", - netatmo_name="Rain", - enable_default=True, - unit=LENGTH_MILLIMETERS, + NetatmoSensorEntityDescription( + key="rain", + name="Rain", + _netatmo_name="Rain", + entity_registry_enabled_default=True, + unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), - "sum_rain_1": SensorMetadata( - "Rain last hour", - enable_default=False, - netatmo_name="sum_rain_1", - unit=LENGTH_MILLIMETERS, + NetatmoSensorEntityDescription( + key="sum_rain_1", + name="Rain last hour", + _netatmo_name="sum_rain_1", + entity_registry_enabled_default=False, + unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), - "sum_rain_24": SensorMetadata( - "Rain today", - enable_default=True, - netatmo_name="sum_rain_24", - unit=LENGTH_MILLIMETERS, + NetatmoSensorEntityDescription( + key="sum_rain_24", + name="Rain today", + _netatmo_name="sum_rain_24", + entity_registry_enabled_default=True, + unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), - "battery_percent": SensorMetadata( - "Battery Percent", - netatmo_name="battery_percent", - enable_default=True, - unit=PERCENTAGE, + NetatmoSensorEntityDescription( + key="battery_percent", + name="Battery Percent", + _netatmo_name="battery_percent", + entity_registry_enabled_default=True, + unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, ), - "windangle": SensorMetadata( - "Direction", - netatmo_name="WindAngle", - enable_default=True, + NetatmoSensorEntityDescription( + key="windangle", + name="Direction", + _netatmo_name="WindAngle", + entity_registry_enabled_default=True, icon="mdi:compass-outline", ), - "windangle_value": SensorMetadata( - "Angle", - netatmo_name="WindAngle", - enable_default=False, - unit=DEGREE, + NetatmoSensorEntityDescription( + key="windangle_value", + name="Angle", + _netatmo_name="WindAngle", + entity_registry_enabled_default=False, + unit_of_measurement=DEGREE, icon="mdi:compass-outline", ), - "windstrength": SensorMetadata( - "Wind Strength", - netatmo_name="WindStrength", - enable_default=True, - unit=SPEED_KILOMETERS_PER_HOUR, + NetatmoSensorEntityDescription( + key="windstrength", + name="Wind Strength", + _netatmo_name="WindStrength", + entity_registry_enabled_default=True, + unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", ), - "gustangle": SensorMetadata( - "Gust Direction", - netatmo_name="GustAngle", - enable_default=False, + NetatmoSensorEntityDescription( + key="gustangle", + name="Gust Direction", + _netatmo_name="GustAngle", + entity_registry_enabled_default=False, icon="mdi:compass-outline", ), - "gustangle_value": SensorMetadata( - "Gust Angle", - netatmo_name="GustAngle", - enable_default=False, - unit=DEGREE, + NetatmoSensorEntityDescription( + key="gustangle_value", + name="Gust Angle", + _netatmo_name="GustAngle", + entity_registry_enabled_default=False, + unit_of_measurement=DEGREE, icon="mdi:compass-outline", ), - "guststrength": SensorMetadata( - "Gust Strength", - netatmo_name="GustStrength", - enable_default=False, - unit=SPEED_KILOMETERS_PER_HOUR, + NetatmoSensorEntityDescription( + key="guststrength", + name="Gust Strength", + _netatmo_name="GustStrength", + entity_registry_enabled_default=False, + unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", ), - "reachable": SensorMetadata( - "Reachability", - netatmo_name="reachable", - enable_default=False, + NetatmoSensorEntityDescription( + key="reachable", + name="Reachability", + _netatmo_name="reachable", + entity_registry_enabled_default=False, icon="mdi:signal", ), - "rf_status": SensorMetadata( - "Radio", - netatmo_name="rf_status", - enable_default=False, + NetatmoSensorEntityDescription( + key="rf_status", + name="Radio", + _netatmo_name="rf_status", + entity_registry_enabled_default=False, icon="mdi:signal", ), - "rf_status_lvl": SensorMetadata( - "Radio Level", - netatmo_name="rf_status", - enable_default=False, - unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + NetatmoSensorEntityDescription( + key="rf_status_lvl", + name="Radio Level", + _netatmo_name="rf_status", + entity_registry_enabled_default=False, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), - "wifi_status": SensorMetadata( - "Wifi", - netatmo_name="wifi_status", - enable_default=False, + NetatmoSensorEntityDescription( + key="wifi_status", + name="Wifi", + _netatmo_name="wifi_status", + entity_registry_enabled_default=False, icon="mdi:wifi", ), - "wifi_status_lvl": SensorMetadata( - "Wifi Level", - netatmo_name="wifi_status", - enable_default=False, - unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + NetatmoSensorEntityDescription( + key="wifi_status_lvl", + name="Wifi Level", + _netatmo_name="wifi_status", + entity_registry_enabled_default=False, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), - "health_idx": SensorMetadata( - "Health", - enable_default=True, - netatmo_name="health_idx", + NetatmoSensorEntityDescription( + key="health_idx", + name="Health", + _netatmo_name="health_idx", + entity_registry_enabled_default=True, icon="mdi:cloud", ), -} +) +SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES] MODULE_TYPE_OUTDOOR = "NAModule1" MODULE_TYPE_WIND = "NAModule2" @@ -307,18 +334,21 @@ async def async_setup_entry( conditions = [ c.lower() for c in data_class.get_monitored_conditions(module_id=module["_id"]) - if c.lower() in SENSOR_TYPES + if c.lower() in SENSOR_TYPES_KEYS ] for condition in conditions: - if f"{condition}_value" in SENSOR_TYPES: + if f"{condition}_value" in SENSOR_TYPES_KEYS: conditions.append(f"{condition}_value") - elif f"{condition}_lvl" in SENSOR_TYPES: + elif f"{condition}_lvl" in SENSOR_TYPES_KEYS: conditions.append(f"{condition}_lvl") - for condition in conditions: - entities.append( - NetatmoSensor(data_handler, data_class_name, module, condition) - ) + entities.extend( + [ + NetatmoSensor(data_handler, data_class_name, module, description) + for description in SENSOR_TYPES + if description.key in conditions + ] + ) _LOGGER.debug("Adding weather sensors %s", entities) return entities @@ -379,10 +409,13 @@ async def async_setup_entry( nonlocal platform_not_ready platform_not_ready = False - for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: - new_entities.append( - NetatmoPublicSensor(data_handler, area, sensor_type) - ) + new_entities.extend( + [ + NetatmoPublicSensor(data_handler, area, description) + for description in SENSOR_TYPES + if description.key in SUPPORTED_PUBLIC_SENSOR_TYPES + ] + ) for device_id in entities.values(): device_registry.async_remove_device(device_id) @@ -403,17 +436,18 @@ async def async_setup_entry( class NetatmoSensor(NetatmoBase, SensorEntity): """Implementation of a Netatmo sensor.""" + entity_description: NetatmoSensorEntityDescription + def __init__( self, data_handler: NetatmoDataHandler, data_class_name: str, module_info: dict, - sensor_type: str, + description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data_handler) - - metadata: SensorMetadata = SENSOR_TYPES[sensor_type] + self.entity_description = description self._data_classes.append( {"name": data_class_name, SIGNAL_NAME: data_class_name} @@ -437,14 +471,9 @@ class NetatmoSensor(NetatmoBase, SensorEntity): f"{module_info.get('module_name', device['type'])}" ) - self._attr_name = f"{MANUFACTURER} {self._device_name} {metadata.name}" - self.type = sensor_type - self._attr_device_class = metadata.device_class - self._attr_icon = metadata.icon - self._attr_unit_of_measurement = metadata.unit + self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" self._model = device["type"] - self._attr_unique_id = f"{self._id}-{self.type}" - self._attr_entity_registry_enabled_default = metadata.enable_default + self._attr_unique_id = f"{self._id}-{description.key}" @property def _data(self) -> pyatmo.AsyncWeatherStationData: @@ -478,24 +507,28 @@ class NetatmoSensor(NetatmoBase, SensorEntity): return try: - state = data[SENSOR_TYPES[self.type].netatmo_name] - if self.type in {"temperature", "pressure", "sum_rain_1"}: + state = data[self.entity_description.netatmo_name] + if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}: self._attr_state = round(state, 1) - elif self.type in {"windangle_value", "gustangle_value"}: + elif self.entity_description.key in {"windangle_value", "gustangle_value"}: self._attr_state = fix_angle(state) - elif self.type in {"windangle", "gustangle"}: + elif self.entity_description.key in {"windangle", "gustangle"}: self._attr_state = process_angle(fix_angle(state)) - elif self.type == "rf_status": + elif self.entity_description.key == "rf_status": self._attr_state = process_rf(state) - elif self.type == "wifi_status": + elif self.entity_description.key == "wifi_status": self._attr_state = process_wifi(state) - elif self.type == "health_idx": + elif self.entity_description.key == "health_idx": self._attr_state = process_health(state) else: self._attr_state = state except KeyError: if self.state: - _LOGGER.debug("No %s data found for %s", self.type, self._device_name) + _LOGGER.debug( + "No %s data found for %s", + self.entity_description.key, + self._device_name, + ) self._attr_state = None return @@ -583,11 +616,17 @@ def process_wifi(strength: int) -> str: class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Represent a single sensor in a Netatmo.""" + entity_description: NetatmoSensorEntityDescription + def __init__( - self, data_handler: NetatmoDataHandler, area: NetatmoArea, sensor_type: str + self, + data_handler: NetatmoDataHandler, + area: NetatmoArea, + description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data_handler) + self.entity_description = description self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" @@ -602,20 +641,17 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): SIGNAL_NAME: self._signal_name, } ) - metadata: SensorMetadata = SENSOR_TYPES[sensor_type] - self.type = sensor_type self.area = area self._mode = area.mode self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = f"{MANUFACTURER} {self._device_name} {metadata.name}" - self._attr_device_class = metadata.device_class - self._attr_icon = metadata.icon - self._attr_unit_of_measurement = metadata.unit + self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" self._show_on_map = area.show_on_map - self._attr_unique_id = f"{self._device_name.replace(' ', '-')}-{self.type}" + self._attr_unique_id = ( + f"{self._device_name.replace(' ', '-')}-{description.key}" + ) self._model = PUBLIC self._attr_extra_state_attributes.update( @@ -682,28 +718,30 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Update the entity's state.""" data = None - if self.type == "temperature": + if self.entity_description.key == "temperature": data = self._data.get_latest_temperatures() - elif self.type == "pressure": + elif self.entity_description.key == "pressure": data = self._data.get_latest_pressures() - elif self.type == "humidity": + elif self.entity_description.key == "humidity": data = self._data.get_latest_humidities() - elif self.type == "rain": + elif self.entity_description.key == "rain": data = self._data.get_latest_rain() - elif self.type == "sum_rain_1": + elif self.entity_description.key == "sum_rain_1": data = self._data.get_60_min_rain() - elif self.type == "sum_rain_24": + elif self.entity_description.key == "sum_rain_24": data = self._data.get_24_h_rain() - elif self.type == "windstrength": + elif self.entity_description.key == "windstrength": data = self._data.get_latest_wind_strengths() - elif self.type == "guststrength": + elif self.entity_description.key == "guststrength": data = self._data.get_latest_gust_strengths() if data is None: if self.state is None: return _LOGGER.debug( - "No station provides %s data in the area %s", self.type, self._area_name + "No station provides %s data in the area %s", + self.entity_description.key, + self._area_name, ) self._attr_state = None return From 9b67605b8a4dff4b3d272f26577da257a9005c43 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 28 Jul 2021 08:21:00 +0200 Subject: [PATCH 675/818] Use SensorEntityDescription in GIOS integration (#53581) --- homeassistant/components/gios/const.py | 94 ++++++++++++++----------- homeassistant/components/gios/model.py | 14 ++-- homeassistant/components/gios/sensor.py | 56 +++++++-------- 3 files changed, 87 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 834735c6189..9b890442166 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,10 +4,10 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER -from .model import SensorDescription +from .model import GiosSensorEntityDescription ATTRIBUTION: Final = "Data provided by GIOŚ" @@ -22,9 +22,6 @@ API_TIMEOUT: Final = 30 ATTR_INDEX: Final = "index" ATTR_STATION: Final = "station" -ATTR_UNIT: Final = "unit" -ATTR_VALUE: Final = "value" -ATTR_STATION_NAME: Final = "station_name" ATTR_C6H6: Final = "c6h6" ATTR_CO: Final = "co" @@ -35,41 +32,52 @@ ATTR_PM25: Final = "pm25" ATTR_SO2: Final = "so2" ATTR_AQI: Final = "aqi" -SENSOR_TYPES: Final[dict[str, SensorDescription]] = { - ATTR_AQI: {}, - ATTR_C6H6: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_CO: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_NO2: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_O3: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_PM10: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_PM25: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, - ATTR_SO2: { - ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_VALUE: round, - }, -} +SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( + GiosSensorEntityDescription( + key=ATTR_AQI, + name="AQI", + value=None, + ), + GiosSensorEntityDescription( + key=ATTR_C6H6, + name="C6H6", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_CO, + name="CO", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_NO2, + name="NO2", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_O3, + name="O3", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM10, + name="PM10", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM25, + name="PM2.5", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_SO2, + name="SO2", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), +) diff --git a/homeassistant/components/gios/model.py b/homeassistant/components/gios/model.py index 867c950e183..b6ae9a9f78f 100644 --- a/homeassistant/components/gios/model.py +++ b/homeassistant/components/gios/model.py @@ -1,12 +1,14 @@ """Type definitions for GIOS integration.""" from __future__ import annotations -from typing import Callable, TypedDict +from dataclasses import dataclass +from typing import Callable + +from homeassistant.components.sensor import SensorEntityDescription -class SensorDescription(TypedDict, total=False): - """Sensor description class.""" +@dataclass +class GiosSensorEntityDescription(SensorEntityDescription): + """Class describing GIOS sensor entities.""" - unit: str - state_class: str - value: Callable + value: Callable | None = round diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 4ab7facec9f..b651112b9db 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -4,11 +4,7 @@ from __future__ import annotations import logging from typing import Any, cast -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN as PLATFORM, - SensorEntity, -) +from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME from homeassistant.core import HomeAssistant @@ -23,14 +19,13 @@ from .const import ( ATTR_INDEX, ATTR_PM25, ATTR_STATION, - ATTR_UNIT, - ATTR_VALUE, ATTRIBUTION, DEFAULT_NAME, DOMAIN, MANUFACTURER, SENSOR_TYPES, ) +from .model import GiosSensorEntityDescription _LOGGER = logging.getLogger(__name__) @@ -61,13 +56,13 @@ async def async_setup_entry( sensors: list[GiosSensor | GiosAqiSensor] = [] - for sensor in SENSOR_TYPES: - if getattr(coordinator.data, sensor) is None: + for description in SENSOR_TYPES: + if getattr(coordinator.data, description.key) is None: continue - if sensor == ATTR_AQI: - sensors.append(GiosAqiSensor(name, sensor, coordinator)) + if description.key == ATTR_AQI: + sensors.append(GiosAqiSensor(name, coordinator, description)) else: - sensors.append(GiosSensor(name, sensor, coordinator)) + sensors.append(GiosSensor(name, coordinator, description)) async_add_entities(sensors) @@ -75,13 +70,16 @@ class GiosSensor(CoordinatorEntity, SensorEntity): """Define an GIOS sensor.""" coordinator: GiosDataUpdateCoordinator + entity_description: GiosSensorEntityDescription def __init__( - self, name: str, sensor_type: str, coordinator: GiosDataUpdateCoordinator + self, + name: str, + coordinator: GiosDataUpdateCoordinator, + description: GiosSensorEntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) - self._description = SENSOR_TYPES[sensor_type] self._attr_device_info = { "identifiers": {(DOMAIN, str(coordinator.gios.station_id))}, "name": DEFAULT_NAME, @@ -89,33 +87,31 @@ class GiosSensor(CoordinatorEntity, SensorEntity): "entry_type": "service", } self._attr_icon = "mdi:blur" - if sensor_type == ATTR_PM25: - self._attr_name = f"{name} PM2.5" - else: - self._attr_name = f"{name} {sensor_type.upper()}" - self._attr_state_class = self._description.get(ATTR_STATE_CLASS) - self._attr_unique_id = f"{coordinator.gios.station_id}-{sensor_type}" - self._attr_unit_of_measurement = self._description.get(ATTR_UNIT) - self._sensor_type = sensor_type + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.coordinator.gios.station_name, } + self.entity_description = description @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - self._attrs[ATTR_NAME] = getattr(self.coordinator.data, self._sensor_type).name + self._attrs[ATTR_NAME] = getattr( + self.coordinator.data, self.entity_description.key + ).name self._attrs[ATTR_INDEX] = getattr( - self.coordinator.data, self._sensor_type + self.coordinator.data, self.entity_description.key ).index return self._attrs @property def state(self) -> StateType: """Return the state.""" - state = getattr(self.coordinator.data, self._sensor_type).value - return cast(StateType, self._description[ATTR_VALUE](state)) + state = getattr(self.coordinator.data, self.entity_description.key).value + assert self.entity_description.value is not None + return cast(StateType, self.entity_description.value(state)) class GiosAqiSensor(GiosSensor): @@ -124,10 +120,14 @@ class GiosAqiSensor(GiosSensor): @property def state(self) -> StateType: """Return the state.""" - return cast(StateType, getattr(self.coordinator.data, self._sensor_type).value) + return cast( + StateType, getattr(self.coordinator.data, self.entity_description.key).value + ) @property def available(self) -> bool: """Return if entity is available.""" available = super().available - return available and bool(getattr(self.coordinator.data, self._sensor_type)) + return available and bool( + getattr(self.coordinator.data, self.entity_description.key) + ) From d865577187534f54ade6773c0dcfb8cc31757bfb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 28 Jul 2021 01:26:26 -0500 Subject: [PATCH 676/818] Bump plexapi to 4.7.0 (#53597) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 3de7895a805..40d8ecc675e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.6.1", + "plexapi==4.7.0", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index 320a4377d0c..c8d1df946e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ pillow==8.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.6.1 +plexapi==4.7.0 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98dc1f34e9f..22f09c13d0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -656,7 +656,7 @@ pilight==0.1.1 pillow==8.2.0 # homeassistant.components.plex -plexapi==4.6.1 +plexapi==4.7.0 # homeassistant.components.plex plexauth==0.0.6 From 9c338183926d07215323bcd8dc88e63daaaf0197 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 27 Jul 2021 23:43:24 -0700 Subject: [PATCH 677/818] Improve CO2Signal error handling (#53602) * Improve CO2Signal error handling * Update homeassistant/components/co2signal/sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/co2signal/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index e7ac138335b..bd8d94355fd 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -114,6 +114,13 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE f"{coordinator.entry_id}_{description.unique_id or description.key}" ) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available and self._description.key in self.coordinator.data["data"] + ) + @property def state(self) -> StateType: """Return sensor state.""" From 514d97f1449a769e6f5296e9f4b232856fb3777c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Jul 2021 00:51:40 -0600 Subject: [PATCH 678/818] Enforce strict typing for ReCollect Waste (#53356) --- .strict-typing | 1 + .../components/recollect_waste/manifest.json | 2 +- mypy.ini | 14 +++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/mypy_config.py | 1 - 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.strict-typing b/.strict-typing index 9cfe0b5a9a7..40d69f7cb56 100644 --- a/.strict-typing +++ b/.strict-typing @@ -75,6 +75,7 @@ homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.proximity.* homeassistant.components.rainmachine.* +homeassistant.components.recollect_waste.* homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 550612fbea2..258d74915f7 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -3,7 +3,7 @@ "name": "ReCollect Waste", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": ["aiorecollect==1.0.5"], + "requirements": ["aiorecollect==1.0.7"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/mypy.ini b/mypy.ini index 613a4e3335d..e5c9ccca2dd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -836,6 +836,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recollect_waste.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.purge] check_untyped_defs = true disallow_incomplete_defs = true @@ -1576,9 +1587,6 @@ ignore_errors = true [mypy-homeassistant.components.rachio.*] ignore_errors = true -[mypy-homeassistant.components.recollect_waste.*] -ignore_errors = true - [mypy-homeassistant.components.reddit.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index c8d1df946e3..f99e4ad57ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -236,7 +236,7 @@ aiopvpc==2.2.0 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.5 +aiorecollect==1.0.7 # homeassistant.components.shelly aioshelly==0.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22f09c13d0d..12f4e7296ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aiopvpc==2.2.0 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.5 +aiorecollect==1.0.7 # homeassistant.components.shelly aioshelly==0.6.4 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b5945d2a6da..8ff72c332da 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -136,7 +136,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.profiler.*", "homeassistant.components.proxmoxve.*", "homeassistant.components.rachio.*", - "homeassistant.components.recollect_waste.*", "homeassistant.components.reddit.*", "homeassistant.components.ring.*", "homeassistant.components.rpi_power.*", From 1968b95829bbcfdf04edb0db6a2c1e511907b07b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Jul 2021 08:55:58 +0200 Subject: [PATCH 679/818] Add currency core configuration (#53541) Co-authored-by: Paulus Schoutsen --- homeassistant/components/api/__init__.py | 2 + homeassistant/components/config/core.py | 1 + homeassistant/config.py | 4 + homeassistant/core.py | 7 + homeassistant/helpers/config_validation.py | 164 +++++++++++++++++++++ tests/components/config/test_core.py | 5 +- tests/helpers/test_config_validation.py | 15 ++ tests/test_config.py | 12 +- tests/test_core.py | 2 + 9 files changed, 210 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index a91d8540286..0a11cf04651 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -43,6 +43,7 @@ from homeassistant.helpers.system_info import async_get_system_info _LOGGER = logging.getLogger(__name__) ATTR_BASE_URL = "base_url" +ATTR_CURRENCY = "currency" ATTR_EXTERNAL_URL = "external_url" ATTR_INTERNAL_URL = "internal_url" ATTR_LOCATION_NAME = "location_name" @@ -195,6 +196,7 @@ class APIDiscoveryView(HomeAssistantView): # always needs authentication ATTR_REQUIRES_API_PASSWORD: True, ATTR_VERSION: __version__, + ATTR_CURRENCY: None, } with suppress(NoURLAvailableError): diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 3b38ab49c4b..d9029dc497f 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -46,6 +46,7 @@ class CheckConfigView(HomeAssistantView): vol.Optional("time_zone"): cv.time_zone, vol.Optional("external_url"): vol.Any(cv.url, None), vol.Optional("internal_url"): vol.Any(cv.url, None), + vol.Optional("currency"): cv.currency, } ) async def websocket_update_config(hass, connection, msg): diff --git a/homeassistant/config.py b/homeassistant/config.py index 9e128fc6d23..8641a80ddef 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_ALLOWLIST_EXTERNAL_URLS, CONF_AUTH_MFA_MODULES, CONF_AUTH_PROVIDERS, + CONF_CURRENCY, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, @@ -238,6 +239,7 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( # pylint: disable=no-value-for-parameter vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, + vol.Optional(CONF_CURRENCY): cv.currency, } ) @@ -520,6 +522,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non CONF_UNIT_SYSTEM, CONF_EXTERNAL_URL, CONF_INTERNAL_URL, + CONF_CURRENCY, ) ): hac.config_source = SOURCE_YAML @@ -533,6 +536,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non (CONF_EXTERNAL_URL, "external_url"), (CONF_MEDIA_DIRS, "media_dirs"), (CONF_LEGACY_TEMPLATES, "legacy_templates"), + (CONF_CURRENCY, "currency"), ): if key in config: setattr(hac, attr, config[key]) diff --git a/homeassistant/core.py b/homeassistant/core.py index aba4483f192..e2418321592 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1545,6 +1545,7 @@ class Config: self.units: UnitSystem = METRIC_SYSTEM self.internal_url: str | None = None self.external_url: str | None = None + self.currency: str = "EUR" self.config_source: str = "default" @@ -1650,6 +1651,7 @@ class Config: "state": self.hass.state.value, "external_url": self.external_url, "internal_url": self.internal_url, + "currency": self.currency, } def set_time_zone(self, time_zone_str: str) -> None: @@ -1676,6 +1678,7 @@ class Config: # pylint: disable=dangerous-default-value # _UNDEFs not modified external_url: str | dict | None = _UNDEF, internal_url: str | dict | None = _UNDEF, + currency: str | None = None, ) -> None: """Update the configuration from a dictionary.""" self.config_source = source @@ -1698,6 +1701,8 @@ class Config: self.external_url = cast(Optional[str], external_url) if internal_url is not _UNDEF: self.internal_url = cast(Optional[str], internal_url) + if currency is not None: + self.currency = currency async def async_update(self, **kwargs: Any) -> None: """Update the configuration from a dictionary.""" @@ -1723,6 +1728,7 @@ class Config: time_zone=data.get("time_zone"), external_url=data.get("external_url", _UNDEF), internal_url=data.get("internal_url", _UNDEF), + currency=data.get("currency"), ) async def async_store(self) -> None: @@ -1736,6 +1742,7 @@ class Config: "time_zone": self.time_zone, "external_url": self.external_url, "internal_url": self.internal_url, + "currency": self.currency, } store = self.hass.helpers.storage.Store( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e195c1ded31..66d1c01d6d3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1269,3 +1269,167 @@ ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = { SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA, SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA, } + + +# Validate currencies adopted by countries +currency = vol.In( + { + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYR", + "BZD", + "CAD", + "CDF", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMK", + "ZWL", + }, + msg="invalid ISO 4217 formatted currency", +) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 54fd6a76565..9c86a3f2d1b 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,4 +1,4 @@ -"""Test hassbian config.""" +"""Test core config.""" from unittest.mock import patch import pytest @@ -60,6 +60,7 @@ async def test_websocket_core_update(hass, client): assert hass.config.time_zone != "America/New_York" assert hass.config.external_url != "https://www.example.com" assert hass.config.internal_url != "http://example.com" + assert hass.config.currency == "EUR" with patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz: await client.send_json( @@ -74,6 +75,7 @@ async def test_websocket_core_update(hass, client): "time_zone": "America/New_York", "external_url": "https://www.example.com", "internal_url": "http://example.local", + "currency": "USD", } ) @@ -89,6 +91,7 @@ async def test_websocket_core_update(hass, client): assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" + assert hass.config.currency == "USD" assert len(mock_set_tz.mock_calls) == 1 assert mock_set_tz.mock_calls[0][1][0] == dt_util.get_time_zone("America/New_York") diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 02303825bbd..c5e9f5880c4 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1085,3 +1085,18 @@ def test_whitespace(): for value in (" ", " "): assert schema(value) + + +def test_currency(): + """Test currency validator.""" + schema = vol.Schema(cv.currency) + + for value in ( + None, + "BTC", + ): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ("EUR", "USD"): + assert schema(value) diff --git a/tests/test_config.py b/tests/test_config.py index 87496c566e3..c1eb1ab7540 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -193,6 +193,7 @@ def test_core_config_schema(): {"longitude": -181}, {"external_url": "not an url"}, {"internal_url": "not an url"}, + {"currency", 100}, {"customize": "bla"}, {"customize": {"light.sensor": 100}}, {"customize": {"entity_id": []}}, @@ -208,6 +209,7 @@ def test_core_config_schema(): "external_url": "https://www.example.com", "internal_url": "http://example.local", CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + "currency": "USD", "customize": {"sensor.temperature": {"hidden": True}}, } ) @@ -360,6 +362,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): "unit_system": "metric", "external_url": "https://www.example.com", "internal_url": "http://example.local", + "currency": "EUR", }, "key": "core.config", "version": 1, @@ -376,6 +379,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.time_zone == "Europe/Copenhagen" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" + assert hass.config.currency == "EUR" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source == SOURCE_STORAGE @@ -423,6 +427,7 @@ async def test_updating_configuration(hass, hass_storage): "unit_system": "metric", "external_url": "https://www.example.com", "internal_url": "http://example.local", + "currency": "BTC", }, "key": "core.config", "version": 1, @@ -431,12 +436,14 @@ async def test_updating_configuration(hass, hass_storage): await config_util.async_process_ha_core_config( hass, {"allowlist_external_dirs": "/etc"} ) - await hass.config.async_update(latitude=50) + await hass.config.async_update(latitude=50, currency="USD") new_core_data = copy.deepcopy(core_data) new_core_data["data"]["latitude"] = 50 + new_core_data["data"]["currency"] = "USD" assert hass_storage["core.config"] == new_core_data assert hass.config.latitude == 50 + assert hass.config.currency == "USD" async def test_override_stored_configuration(hass, hass_storage): @@ -484,6 +491,7 @@ async def test_loading_configuration(hass): "internal_url": "http://example.local", "media_dirs": {"mymedia": "/usr"}, "legacy_templates": True, + "currency": "EUR", }, ) @@ -501,6 +509,7 @@ async def test_loading_configuration(hass): assert hass.config.media_dirs == {"mymedia": "/usr"} assert hass.config.config_source == config_util.SOURCE_YAML assert hass.config.legacy_templates is True + assert hass.config.currency == "EUR" async def test_loading_configuration_temperature_unit(hass): @@ -528,6 +537,7 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert hass.config.config_source == config_util.SOURCE_YAML + assert hass.config.currency == "EUR" async def test_loading_configuration_default_media_dirs_docker(hass): diff --git a/tests/test_core.py b/tests/test_core.py index 5f17625a3de..77ec07e6a63 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -912,6 +912,7 @@ def test_config_defaults(): assert config.media_dirs == {} assert config.safe_mode is False assert config.legacy_templates is False + assert config.currency == "EUR" def test_config_path_with_file(): @@ -952,6 +953,7 @@ def test_config_as_dict(): "state": "RUNNING", "external_url": None, "internal_url": None, + "currency": "EUR", } assert expected == config.as_dict() From 1aec069f3acb6cc677a9c87fc5079df78caad7b5 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 28 Jul 2021 00:06:41 -0700 Subject: [PATCH 680/818] Use the new EntityDescription for motionEye switches (#53536) --- .../components/motioneye/__init__.py | 12 ++-- homeassistant/components/motioneye/switch.py | 55 ++++++++++++------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 78f9fd7a384..2ade7c48e1b 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -52,7 +52,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.network import get_url from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -442,7 +442,7 @@ class MotionEyeEntity(CoordinatorEntity): client: MotionEyeClient, coordinator: DataUpdateCoordinator, options: MappingProxyType[str, Any], - enabled_by_default: bool = True, + entity_description: EntityDescription = None, ) -> None: """Initialize a motionEye entity.""" self._camera_id = camera[KEY_ID] @@ -457,14 +457,10 @@ class MotionEyeEntity(CoordinatorEntity): self._client = client self._camera: dict[str, Any] | None = camera self._options = options - self._enabled_by_default = enabled_by_default + if entity_description is not None: + self.entity_description = entity_description super().__init__(coordinator) - @property - def entity_registry_enabled_default(self) -> bool: - """Whether or not the entity is enabled by default.""" - return self._enabled_by_default - @property def unique_id(self) -> str: """Return a unique id for this instance.""" diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 4abf00969c9..f9197d00c08 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -18,6 +18,7 @@ from motioneye_client.const import ( from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -25,12 +26,30 @@ from . import MotionEyeEntity, get_camera_from_cameras, listen_for_new_cameras from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_SWITCH_BASE MOTIONEYE_SWITCHES = [ - (KEY_MOTION_DETECTION, "Motion Detection", True), - (KEY_TEXT_OVERLAY, "Text Overlay", False), - (KEY_VIDEO_STREAMING, "Video Streaming", False), - (KEY_STILL_IMAGES, "Still Images", True), - (KEY_MOVIES, "Movies", True), - (KEY_UPLOAD_ENABLED, "Upload Enabled", False), + EntityDescription( + key=KEY_MOTION_DETECTION, + name="Motion Detection", + entity_registry_enabled_default=True, + ), + EntityDescription( + key=KEY_TEXT_OVERLAY, name="Text Overlay", entity_registry_enabled_default=False + ), + EntityDescription( + key=KEY_VIDEO_STREAMING, + name="Video Streaming", + entity_registry_enabled_default=False, + ), + EntityDescription( + key=KEY_STILL_IMAGES, name="Still Images", entity_registry_enabled_default=True + ), + EntityDescription( + key=KEY_MOVIES, name="Movies", entity_registry_enabled_default=True + ), + EntityDescription( + key=KEY_UPLOAD_ENABLED, + name="Upload Enabled", + entity_registry_enabled_default=False, + ), ] @@ -48,14 +67,12 @@ async def async_setup_entry( MotionEyeSwitch( entry.entry_id, camera, - switch_key, - switch_key_friendly_name, entry_data[CONF_CLIENT], entry_data[CONF_COORDINATOR], entry.options, - enabled, + entity_description, ) - for switch_key, switch_key_friendly_name, enabled in MOTIONEYE_SWITCHES + for entity_description in MOTIONEYE_SWITCHES ] ) @@ -69,36 +86,34 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): self, config_entry_id: str, camera: dict[str, Any], - switch_key: str, - switch_key_friendly_name: str, client: MotionEyeClient, coordinator: DataUpdateCoordinator, options: MappingProxyType[str, str], - enabled_by_default: bool, + entity_description: EntityDescription, ) -> None: """Initialize the switch.""" - self._switch_key = switch_key - self._switch_key_friendly_name = switch_key_friendly_name super().__init__( config_entry_id, - f"{TYPE_MOTIONEYE_SWITCH_BASE}_{switch_key}", + f"{TYPE_MOTIONEYE_SWITCH_BASE}_{entity_description.key}", camera, client, coordinator, options, - enabled_by_default, + entity_description, ) @property def name(self) -> str: """Return the name of the switch.""" camera_prepend = f"{self._camera[KEY_NAME]} " if self._camera else "" - return f"{camera_prepend}{self._switch_key_friendly_name}" + return f"{camera_prepend}{self.entity_description.name}" @property def is_on(self) -> bool: """Return true if the switch is on.""" - return bool(self._camera and self._camera.get(self._switch_key, False)) + return bool( + self._camera and self._camera.get(self.entity_description.key, False) + ) async def _async_send_set_camera(self, value: bool) -> None: """Set a switch value.""" @@ -107,7 +122,7 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): # stale configuration. camera = await self._client.async_get_camera(self._camera_id) if camera: - camera[self._switch_key] = value + camera[self.entity_description.key] = value await self._client.async_set_camera(self._camera_id, camera) async def async_turn_on(self, **kwargs: Any) -> None: From 9e219d9b6ef1087f052ac40a476d75012841a87e Mon Sep 17 00:00:00 2001 From: "Richard T. Schaefer" Date: Wed, 28 Jul 2021 02:09:13 -0500 Subject: [PATCH 681/818] Add this variable for use by automation and script templates (#52774) --- .../components/automation/__init__.py | 21 ++++++--- homeassistant/components/script/__init__.py | 7 ++- tests/components/automation/test_init.py | 45 ++++++++++++++++++- tests/components/script/test_init.py | 38 ++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b94029db4ee..a0ff4930b51 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -445,15 +445,19 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trigger_context, self._trace_config, ) as automation_trace: + this = None + state = self.hass.states.get(self.entity_id) + if state: + this = state.as_dict() + variables = {"this": this, **(run_variables or {})} if self._variables: try: - variables = self._variables.async_render(self.hass, run_variables) + variables = self._variables.async_render(self.hass, variables) except template.TemplateError as err: self._logger.error("Error rendering variables: %s", err) automation_trace.set_error(err) return - else: - variables = run_variables + # Prepare tracing the automation automation_trace.set_trace(trace_get()) @@ -569,11 +573,18 @@ class AutomationEntity(ToggleEntity, RestoreEntity): def log_cb(level, msg, **kwargs): self._logger.log(level, "%s %s", msg, self.name, **kwargs) - variables = None + this = None + self.async_write_ha_state() + state = self.hass.states.get(self.entity_id) + if state: + this = state.as_dict() + variables = {"this": this} if self._trigger_variables: try: variables = self._trigger_variables.async_render( - self.hass, None, limited=True + self.hass, + variables, + limited=True, ) except template.TemplateError as err: self._logger.error("Error rendering trigger variables: %s", err) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 41d5e697cf1..483b4065be2 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -401,7 +401,12 @@ class ScriptEntity(ToggleEntity): # Prepare tracing the execution of the script's sequence script_trace.set_trace(trace_get()) with trace_path("sequence"): - return await self.script.async_run(variables, context) + this = None + state = self.hass.states.get(self.entity_id) + if state: + this = state.as_dict() + script_vars = {"this": this, **(variables or {})} + return await self.script.async_run(script_vars, context) async def async_turn_off(self, **kwargs): """Stop running the script. diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 80fe5c52abc..214b2ea20e8 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1184,6 +1184,7 @@ async def test_automation_variables(hass, caplog): "variables": { "test_var": "defined_in_config", "event_type": "{{ trigger.event.event_type }}", + "this_variables": "{{this.entity_id}}", }, "trigger": {"platform": "event", "event_type": "test_event"}, "action": { @@ -1191,6 +1192,8 @@ async def test_automation_variables(hass, caplog): "data": { "value": "{{ test_var }}", "event_type": "{{ event_type }}", + "this_template": "{{this.entity_id}}", + "this_variables": "{{this_variables}}", }, }, }, @@ -1224,6 +1227,11 @@ async def test_automation_variables(hass, caplog): assert len(calls) == 1 assert calls[0].data["value"] == "defined_in_config" assert calls[0].data["event_type"] == "test_event" + # Verify this available to all templates + assert calls[0].data.get("this_template") == "automation.automation_0" + # Verify this available during variables rendering + assert calls[0].data.get("this_variables") == "automation.automation_0" + assert "Error rendering variables" not in caplog.text hass.bus.async_fire("test_event_2") await hass.async_block_till_done() @@ -1276,6 +1284,7 @@ async def test_automation_trigger_variables(hass, caplog): }, "trigger_variables": { "test_var": "defined_in_config", + "this_trigger_variables": "{{this.entity_id}}", }, "trigger": {"platform": "event", "event_type": "test_event_2"}, "action": { @@ -1283,6 +1292,8 @@ async def test_automation_trigger_variables(hass, caplog): "data": { "value": "{{ test_var }}", "event_type": "{{ event_type }}", + "this_template": "{{this.entity_id}}", + "this_trigger_variables": "{{this_trigger_variables}}", }, }, }, @@ -1300,7 +1311,10 @@ async def test_automation_trigger_variables(hass, caplog): assert len(calls) == 2 assert calls[1].data["value"] == "overridden_in_config" assert calls[1].data["event_type"] == "test_event_2" - + # Verify this available to all templates + assert calls[1].data.get("this_template") == "automation.automation_1" + # Verify this available during trigger variables rendering + assert calls[1].data.get("this_trigger_variables") == "automation.automation_1" assert "Error rendering variables" not in caplog.text @@ -1332,6 +1346,35 @@ async def test_automation_bad_trigger_variables(hass, caplog): assert len(calls) == 0 +async def test_automation_this_var_always(hass, caplog): + """Test automation always has reference to this, even with no variable or trigger variables configured.""" + calls = async_mock_service(hass, "test", "automation") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + "data": { + "this_template": "{{this.entity_id}}", + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + # Verify this available to all templates + assert calls[0].data.get("this_template") == "automation.automation_0" + assert "Error rendering variables" not in caplog.text + + async def test_blueprint_automation(hass, calls): """Test blueprint automation.""" assert await async_setup_component( diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 8967a20a01b..6070daeb8af 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -679,6 +679,7 @@ async def test_script_variables(hass, caplog): "script": { "script1": { "variables": { + "this_variable": "{{this.entity_id}}", "test_var": "from_config", "templated_config_var": "{{ var_from_service | default('config-default') }}", }, @@ -688,6 +689,8 @@ async def test_script_variables(hass, caplog): "data": { "value": "{{ test_var }}", "templated_config_var": "{{ templated_config_var }}", + "this_template": "{{this.entity_id}}", + "this_variable": "{{this_variable}}", }, }, ], @@ -731,6 +734,10 @@ async def test_script_variables(hass, caplog): assert len(mock_calls) == 1 assert mock_calls[0].data["value"] == "from_config" assert mock_calls[0].data["templated_config_var"] == "hello" + # Verify this available to all templates + assert mock_calls[0].data.get("this_template") == "script.script1" + # Verify this available during trigger variables rendering + assert mock_calls[0].data.get("this_variable") == "script.script1" await hass.services.async_call( "script", "script1", {"test_var": "from_service"}, blocking=True @@ -758,3 +765,34 @@ async def test_script_variables(hass, caplog): assert len(mock_calls) == 4 assert mock_calls[3].data["value"] == 1 + + +async def test_script_this_var_always(hass, caplog): + """Test script always has reference to this, even with no variabls are configured.""" + + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "sequence": [ + { + "service": "test.script", + "data": { + "this_template": "{{this.entity_id}}", + }, + }, + ], + }, + }, + }, + ) + mock_calls = async_mock_service(hass, "test", "script") + + await hass.services.async_call("script", "script1", blocking=True) + + assert len(mock_calls) == 1 + # Verify this available to all templates + assert mock_calls[0].data.get("this_template") == "script.script1" + assert "Error rendering variables" not in caplog.text From 68945e8814578f3329c2694bc375b1876f61fcc5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Jul 2021 00:12:32 -0700 Subject: [PATCH 682/818] Enable strict static type checking for nest integration (#53535) --- .strict-typing | 1 + homeassistant/components/ffmpeg/__init__.py | 2 +- homeassistant/components/nest/api.py | 3 ++- homeassistant/components/nest/camera_sdm.py | 12 +++++++++--- homeassistant/components/nest/climate_sdm.py | 14 ++++++++++---- homeassistant/components/nest/device_info.py | 4 +++- homeassistant/components/nest/manifest.json | 2 +- mypy.ini | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 40 insertions(+), 13 deletions(-) diff --git a/.strict-typing b/.strict-typing index 40d69f7cb56..e32af5db563 100644 --- a/.strict-typing +++ b/.strict-typing @@ -63,6 +63,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.mysensors.* homeassistant.components.nam.* +homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* homeassistant.components.no_ip.* diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 55e34a547e3..52e034c6265 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -94,7 +94,7 @@ async def async_get_image( input_source: str, output_format: str = IMAGE_JPEG, extra_cmd: str | None = None, -): +) -> bytes | None: """Get an image from a frame of an RTSP stream.""" manager = hass.data[DATA_FFMPEG] ffmpeg = ImageFrame(manager.binary) diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 8affad958b7..426a651461a 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -1,6 +1,7 @@ """API for Google Nest Device Access bound to Home Assistant OAuth.""" import datetime +from typing import cast from aiohttp import ClientSession from google.oauth2.credentials import Credentials @@ -33,7 +34,7 @@ class AsyncConfigEntryAuth(AbstractAuth): """Return a valid access token for SDM API.""" if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() - return self._oauth_session.token["access_token"] + return cast(str, self._oauth_session.token["access_token"]) async def async_get_creds(self) -> Credentials: """Return an OAuth credential for Pub/Sub Subscriber.""" diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 862b6dbdffb..5f5fdbc8d93 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -9,10 +9,11 @@ from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, CameraLiveStreamTrait, - CameraMotionTrait, + EventImageGenerator, RtspStream, ) from google_nest_sdm.device import Device +from google_nest_sdm.event import ImageEventBase from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG @@ -121,6 +122,7 @@ class NestCamera(Camera): _LOGGER.debug("Fetching stream url") self._stream = await trait.generate_rtsp_stream() self._schedule_stream_refresh() + assert self._stream if self._stream.expires_at < utcnow(): _LOGGER.warning("Stream already expired") return self._stream.rtsp_stream_url @@ -198,7 +200,11 @@ class NestCamera(Camera): if not trait: return None # Reuse image bytes if they have already been fetched - event = trait.last_event + if not isinstance(trait, EventImageGenerator): + return None + event: ImageEventBase | None = trait.last_event + if not event: + return None if self._event_id is not None and self._event_id == event.event_id: return self._event_image_bytes _LOGGER.debug("Generating event image URL for event_id %s", event.event_id) @@ -211,7 +217,7 @@ class NestCamera(Camera): return image_bytes async def _async_fetch_active_event_image( - self, trait: CameraMotionTrait + self, trait: EventImageGenerator ) -> bytes | None: """Return image bytes for an active event.""" try: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 51daa7fbb9c..04954cc7a07 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -1,13 +1,14 @@ """Support for Google Nest SDM climate devices.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from google_nest_sdm.device import Device from google_nest_sdm.device_traits import FanTrait, TemperatureTrait from google_nest_sdm.exceptions import GoogleNestException from google_nest_sdm.thermostat_traits import ( ThermostatEcoTrait, + ThermostatHeatCoolTrait, ThermostatHvacTrait, ThermostatModeTrait, ThermostatTemperatureSetpointTrait, @@ -184,15 +185,20 @@ class ThermostatEntity(ClimateEntity): @property def _target_temperature_trait( self, - ) -> ThermostatEcoTrait | ThermostatTemperatureSetpointTrait | None: + ) -> ThermostatHeatCoolTrait | None: """Return the correct trait with a target temp depending on mode.""" if ( self.preset_mode == PRESET_ECO and ThermostatEcoTrait.NAME in self._device.traits ): - return self._device.traits[ThermostatEcoTrait.NAME] + return cast( + ThermostatEcoTrait, self._device.traits[ThermostatEcoTrait.NAME] + ) if ThermostatTemperatureSetpointTrait.NAME in self._device.traits: - return self._device.traits[ThermostatTemperatureSetpointTrait.NAME] + return cast( + ThermostatTemperatureSetpointTrait, + self._device.traits[ThermostatTemperatureSetpointTrait.NAME], + ) return None @property diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 7b10fabcd61..6278547f216 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -61,4 +61,6 @@ class NestDeviceInfo: # The API intentionally returns minimal information about specific # devices, instead relying on traits, but we can infer a generic model # name based on the type - return DEVICE_TYPE_MAP.get(self._device.type, "Unknown") + if self._device.type in DEVICE_TYPE_MAP: + return DEVICE_TYPE_MAP[self._device.type] + return "Unknown" diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index dce39edbacf..05cfa261ef4 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.0"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/mypy.ini b/mypy.ini index e5c9ccca2dd..32800994e42 100644 --- a/mypy.ini +++ b/mypy.ini @@ -704,6 +704,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nest.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.netatmo.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index f99e4ad57ac..eb2781ac5e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -707,7 +707,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.0 +google-nest-sdm==0.3.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12f4e7296ad..ce04a77f864 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.0 +google-nest-sdm==0.3.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 1c20eb3263def048f4f86277bfa83c902cb89b07 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Jul 2021 00:16:23 -0700 Subject: [PATCH 683/818] Skip 'None' values when restoring climate scenes (#53484) --- .../components/climate/reproduce_state.py | 4 +- .../climate/test_reproduce_state.py | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 767a38b2e57..f7e63f475ea 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -41,8 +41,8 @@ async def _async_reproduce_states( data = data or {} data["entity_id"] = state.entity_id for key in keys: - if key in state.attributes: - data[key] = state.attributes[key] + if (value := state.attributes.get(key)) is not None: + data[key] = value await hass.services.async_call( DOMAIN, service, data, blocking=True, context=context diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index bdcf0441b9f..af1b14299ae 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -117,3 +117,57 @@ async def test_attribute(hass, service, attribute): assert len(calls_1) == 1 assert calls_1[0].data == {"entity_id": ENTITY_1, attribute: value} + + +async def test_attribute_partial_temperature(hass): + """Test that service call ignores null attributes.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE) + + await async_reproduce_states( + hass, + [ + State( + ENTITY_1, + None, + { + ATTR_TEMPERATURE: 23.1, + ATTR_TARGET_TEMP_HIGH: None, + ATTR_TARGET_TEMP_LOW: None, + }, + ) + ], + ) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == {"entity_id": ENTITY_1, ATTR_TEMPERATURE: 23.1} + + +async def test_attribute_partial_high_low_temperature(hass): + """Test that service call ignores null attributes.""" + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE) + + await async_reproduce_states( + hass, + [ + State( + ENTITY_1, + None, + { + ATTR_TEMPERATURE: None, + ATTR_TARGET_TEMP_HIGH: 30.1, + ATTR_TARGET_TEMP_LOW: 20.2, + }, + ) + ], + ) + + await hass.async_block_till_done() + + assert len(calls_1) == 1 + assert calls_1[0].data == { + "entity_id": ENTITY_1, + ATTR_TARGET_TEMP_HIGH: 30.1, + ATTR_TARGET_TEMP_LOW: 20.2, + } From 10bfc783651a49ffbe2e7ad8d3e5d5e3223b3180 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 09:41:45 +0200 Subject: [PATCH 684/818] Fix missing encoding with open() (#53593) * Fix missing encoding with open() * Fix tests * Improve open - frontend --- homeassistant/__main__.py | 8 ++++---- homeassistant/components/apns/notify.py | 4 ++-- .../components/bosch_shc/config_flow.py | 2 +- homeassistant/components/config/zwave.py | 2 +- .../components/device_tracker/legacy.py | 2 +- homeassistant/components/file/notify.py | 2 +- homeassistant/components/frontend/__init__.py | 4 +++- homeassistant/components/google/__init__.py | 6 +++--- homeassistant/components/greenwave/light.py | 4 ++-- homeassistant/components/http/ban.py | 2 +- homeassistant/components/kira/__init__.py | 4 ++-- homeassistant/components/light/__init__.py | 2 +- .../components/lutron_caseta/config_flow.py | 4 +++- homeassistant/components/mqtt/__init__.py | 2 +- .../components/python_script/__init__.py | 2 +- .../components/remember_the_milk/__init__.py | 4 ++-- homeassistant/components/webostv/__init__.py | 2 +- homeassistant/config.py | 18 +++++++++--------- tests/components/bosch_shc/test_config_flow.py | 8 ++++++-- tests/components/file/test_notify.py | 2 +- tests/components/http/test_ban.py | 4 +++- 21 files changed, 49 insertions(+), 39 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b01284d9974..177c3a10853 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -146,8 +146,8 @@ def daemonize() -> None: # redirect standard file descriptors to devnull # pylint: disable=consider-using-with - infd = open(os.devnull) - outfd = open(os.devnull, "a+") + infd = open(os.devnull, encoding="utf8") + outfd = open(os.devnull, "a+", encoding="utf8") sys.stdout.flush() sys.stderr.flush() os.dup2(infd.fileno(), sys.stdin.fileno()) @@ -159,7 +159,7 @@ def check_pid(pid_file: str) -> None: """Check that Home Assistant is not already running.""" # Check pid file try: - with open(pid_file) as file: + with open(pid_file, encoding="utf8") as file: pid = int(file.readline()) except OSError: # PID File does not exist @@ -182,7 +182,7 @@ def write_pid(pid_file: str) -> None: """Create a PID File.""" pid = os.getpid() try: - with open(pid_file, "w") as file: + with open(pid_file, "w", encoding="utf8") as file: file.write(str(pid)) except OSError: print(f"Fatal Error: Unable to write pid file {pid_file}") diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index c9e12a20863..a87cae09b1a 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -184,7 +184,7 @@ class ApnsNotificationService(BaseNotificationService): def write_devices(self): """Write all known devices to file.""" - with open(self.yaml_path, "w+") as out: + with open(self.yaml_path, "w+", encoding="utf8") as out: for device in self.devices.values(): _write_device(out, device) @@ -202,7 +202,7 @@ class ApnsNotificationService(BaseNotificationService): if current_device is None: self.devices[push_id] = device - with open(self.yaml_path, "a") as out: + with open(self.yaml_path, "a", encoding="utf8") as out: _write_device(out, device) return True diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index e795f2bdfec..4415a0ff6ef 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -36,7 +36,7 @@ HOST_SCHEMA = vol.Schema( def write_tls_asset(hass: core.HomeAssistant, filename: str, asset: bytes) -> None: """Write the tls assets to disk.""" makedirs(hass.config.path(DOMAIN), exist_ok=True) - with open(hass.config.path(DOMAIN, filename), "w") as file_handle: + with open(hass.config.path(DOMAIN, filename), "w", encoding="utf8") as file_handle: file_handle.write(asset.decode("utf-8")) diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index dd1bf1f08e2..b0f6fca4817 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -61,7 +61,7 @@ class ZWaveLogView(HomeAssistantView): def _get_log(self, hass, lines): """Retrieve the logfile content.""" logfilepath = hass.config.path(OZW_LOG_FILENAME) - with open(logfilepath) as logfile: + with open(logfilepath, encoding="utf8") as logfile: data = (line.rstrip() for line in logfile) if lines == 0: loglines = list(data) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 333549e82e0..991a4bb7bb1 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -898,7 +898,7 @@ async def async_load_config( def update_config(path: str, dev_id: str, device: Device) -> None: """Add device to YAML configuration file.""" - with open(path, "a") as out: + with open(path, "a", encoding="utf8") as out: device_config = { device.dev_id: { ATTR_NAME: device.name, diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 35c8ebf7df6..adfe15b7a3c 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -41,7 +41,7 @@ class FileNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a file.""" - with open(self.filepath, "a") as file: + with open(self.filepath, "a", encoding="utf8") as file: if os.stat(self.filepath).st_size == 0: title = f"{kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)} notifications (Log started: {dt_util.utcnow().isoformat()})\n{'-' * 80}\n" file.write(title) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 392806dc885..8b92745f4d4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -569,7 +569,9 @@ class IndexView(web_urldispatcher.AbstractResource): """Get template.""" tpl = self._template_cache if tpl is None: - with open(str(_frontend_root(self.repo_path) / "index.html")) as file: + with (_frontend_root(self.repo_path) / "index.html").open( + encoding="utf8" + ) as file: tpl = jinja2.Template(file.read()) # Cache template if not running from repository diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 5ed7bc93b78..6cc7221ba1d 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -244,7 +244,7 @@ def setup(hass, config): def check_correct_scopes(token_file, config): """Check for the correct scopes in file.""" - with open(token_file) as tokenfile: + with open(token_file, encoding="utf8") as tokenfile: contents = tokenfile.read() # Check for quoted scope as our scopes can be subsets of other scopes @@ -408,7 +408,7 @@ def load_config(path): """Load the google_calendar_devices.yaml.""" calendars = {} try: - with open(path) as file: + with open(path, encoding="utf8") as file: data = yaml.safe_load(file) for calendar in data: try: @@ -425,6 +425,6 @@ def load_config(path): def update_config(path, calendar): """Write the google_calendar_devices.yaml.""" - with open(path, "a") as out: + with open(path, "a", encoding="utf8") as out: out.write("\n") yaml.dump([calendar], out, default_flow_style=False) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 41e4b99b6c6..b3d6898d984 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): tokenfile = hass.config.path(".greenwave") if config.get(CONF_VERSION) == 3: if os.path.exists(tokenfile): - with open(tokenfile) as tokenfile: + with open(tokenfile, encoding="utf8") as tokenfile: token = tokenfile.read() else: try: @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except PermissionError: _LOGGER.error("The Gateway Is Not In Sync Mode") raise - with open(tokenfile, "w+") as tokenfile: + with open(tokenfile, "w+", encoding="utf8") as tokenfile: tokenfile.write(token) else: token = None diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 10776f11b00..6e4f5c0a661 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -217,7 +217,7 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> list[IpBa def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: """Update config file with new banned IP address.""" - with open(path, "a") as out: + with open(path, "a", encoding="utf8") as out: ip_ = {str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()}} out.write("\n") out.write(yaml.dump(ip_)) diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 732008e5780..01e584922b6 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -78,7 +78,7 @@ def load_codes(path): """Load KIRA codes from specified file.""" codes = [] if os.path.exists(path): - with open(path) as code_file: + with open(path, encoding="utf8") as code_file: data = yaml.safe_load(code_file) or [] for code in data: try: @@ -87,7 +87,7 @@ def load_codes(path): # keep going _LOGGER.warning("KIRA code invalid data: %s", exception) else: - with open(path, "w") as code_file: + with open(path, "w", encoding="utf8") as code_file: code_file.write("") return codes diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 68e30b39a61..51f387a2cdb 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -586,7 +586,7 @@ class Profiles: for profile_path in profile_paths: if not os.path.isfile(profile_path): continue - with open(profile_path) as inp: + with open(profile_path, encoding="utf8") as inp: reader = csv.reader(inp) # Skip the header diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index ec7010295ec..9d028e97c87 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -137,7 +137,9 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _write_tls_assets(self, assets): """Write the tls assets to disk.""" for asset_key, conf_key in FILE_MAPPING.items(): - with open(self.hass.config.path(self.data[conf_key]), "w") as file_handle: + with open( + self.hass.config.path(self.data[conf_key]), "w", encoding="utf8" + ) as file_handle: file_handle.write(assets[asset_key]) def _tls_assets_exist(self): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e95729602cc..ec5f5f6d1af 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -522,7 +522,7 @@ async def async_setup_entry(hass, entry): unsub = await async_subscribe(hass, call.data["topic"], collect_msg) def write_dump(): - with open(hass.config.path("mqtt_dump.txt"), "wt") as fp: + with open(hass.config.path("mqtt_dump.txt"), "wt", encoding="utf8") as fp: for msg in messages: fp.write(",".join(msg) + "\n") diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 2051b32b63f..89a7ab4ba04 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -139,7 +139,7 @@ def execute_script(hass, name, data=None): """Execute a script.""" filename = f"{name}.py" raise_if_invalid_filename(filename) - with open(hass.config.path(FOLDER, filename)) as fil: + with open(hass.config.path(FOLDER, filename), encoding="utf8") as fil: source = fil.read() execute(hass, filename, source, data) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 9de33c67158..43658db907c 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -163,7 +163,7 @@ class RememberTheMilkConfiguration: return try: _LOGGER.debug("Loading configuration from file: %s", self._config_file_path) - with open(self._config_file_path) as config_file: + with open(self._config_file_path, encoding="utf8") as config_file: self._config = json.load(config_file) except ValueError: _LOGGER.error( @@ -174,7 +174,7 @@ class RememberTheMilkConfiguration: def save_config(self): """Write the configuration to a file.""" - with open(self._config_file_path, "w") as config_file: + with open(self._config_file_path, "w", encoding="utf8") as config_file: json.dump(self._config, config_file) def get_token(self, profile_name): diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 96559fe5a68..db5a618ff5c 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -112,7 +112,7 @@ def convert_client_keys(config_file): return # Try to parse the file as being JSON - with open(config_file) as json_file: + with open(config_file, encoding="utf8") as json_file: try: json_conf = json.load(json_file) except (json.JSONDecodeError, UnicodeDecodeError): diff --git a/homeassistant/config.py b/homeassistant/config.py index 8641a80ddef..e7b6e04e8cf 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -290,30 +290,30 @@ def _write_default_config(config_dir: str) -> bool: # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. try: - with open(config_path, "wt") as config_file: + with open(config_path, "wt", encoding="utf8") as config_file: config_file.write(DEFAULT_CONFIG) if not os.path.isfile(secret_path): - with open(secret_path, "wt") as secret_file: + with open(secret_path, "wt", encoding="utf8") as secret_file: secret_file.write(DEFAULT_SECRETS) - with open(version_path, "wt") as version_file: + with open(version_path, "wt", encoding="utf8") as version_file: version_file.write(__version__) if not os.path.isfile(group_yaml_path): - with open(group_yaml_path, "wt"): + with open(group_yaml_path, "wt", encoding="utf8"): pass if not os.path.isfile(automation_yaml_path): - with open(automation_yaml_path, "wt") as automation_file: + with open(automation_yaml_path, "wt", encoding="utf8") as automation_file: automation_file.write("[]") if not os.path.isfile(script_yaml_path): - with open(script_yaml_path, "wt"): + with open(script_yaml_path, "wt", encoding="utf8"): pass if not os.path.isfile(scene_yaml_path): - with open(scene_yaml_path, "wt"): + with open(scene_yaml_path, "wt", encoding="utf8"): pass return True @@ -379,7 +379,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: version_path = hass.config.path(VERSION_FILE) try: - with open(version_path) as inp: + with open(version_path, encoding="utf8") as inp: conf_version = inp.readline().strip() except FileNotFoundError: # Last version to not have this file @@ -423,7 +423,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: if os.path.isdir(lib_path): shutil.rmtree(lib_path) - with open(version_path, "wt") as outp: + with open(version_path, "wt", encoding="utf8") as outp: outp.write(__version__) diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index c75814aabc3..0e760b899c1 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -617,9 +617,13 @@ async def test_tls_assets_writer(hass): } with patch("os.mkdir"), patch("builtins.open", mock_open()) as mocked_file: write_tls_asset(hass, CONF_SHC_CERT, assets["cert"]) - mocked_file.assert_called_with(hass.config.path(DOMAIN, CONF_SHC_CERT), "w") + mocked_file.assert_called_with( + hass.config.path(DOMAIN, CONF_SHC_CERT), "w", encoding="utf8" + ) mocked_file().write.assert_called_with("content_cert") write_tls_asset(hass, CONF_SHC_KEY, assets["key"]) - mocked_file.assert_called_with(hass.config.path(DOMAIN, CONF_SHC_KEY), "w") + mocked_file.assert_called_with( + hass.config.path(DOMAIN, CONF_SHC_KEY), "w", encoding="utf8" + ) mocked_file().write.assert_called_with("content_key") diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index d2db5d9e8a8..9650c6945a6 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -63,7 +63,7 @@ async def test_notify_file(hass, timestamp): full_filename = os.path.join(hass.config.path(), filename) assert m_open.call_count == 1 - assert m_open.call_args == call(full_filename, "a") + assert m_open.call_args == call(full_filename, "a", encoding="utf8") assert m_open.return_value.write.call_count == 2 if not timestamp: diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 717bd9564c0..e3c2abf2293 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -166,7 +166,9 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): resp = await client.get("/") assert resp.status == 401 assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1 - m_open.assert_called_once_with(hass.config.path(IP_BANS_FILE), "a") + m_open.assert_called_once_with( + hass.config.path(IP_BANS_FILE), "a", encoding="utf8" + ) resp = await client.get("/") assert resp.status == HTTP_FORBIDDEN From d58151034c5873252422a184e4786ae433395be9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Jul 2021 01:20:51 -0700 Subject: [PATCH 685/818] Combine some stream test mocks (#53600) Combine MockFlushPart with the FakePyAvContainer since the container is effectively a mock AvOutput. This simulates the behavior of the call to mux and close that actually write to the memory file. --- tests/components/stream/test_worker.py | 30 ++++++-------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index d5f5ef653c6..ffbeb44d79e 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -149,6 +149,7 @@ class FakePyAvBuffer: self.segments = [] self.audio_packets = [] self.video_packets = [] + self.memory_file: io.BytesIO | None = None def add_stream(self, template=None): """Create an output buffer that captures packets for test to examine.""" @@ -171,10 +172,13 @@ class FakePyAvBuffer: """Capture a packet for tests to examine.""" # Forward to appropriate FakeStream packet.stream.mux(packet) + # Make new init/part data available to the worker + self.memory_file.write(b"0") def close(self): """Close the buffer.""" - return + # Make the final segment data available to the worker + self.memory_file.write(b"0") def capture_output_segment(self, segment): """Capture the output segment for tests to inspect.""" @@ -201,23 +205,11 @@ class MockPyAv: def open(self, stream_source, *args, **kwargs): """Return a stream or buffer depending on args.""" if isinstance(stream_source, io.BytesIO): + self.capture_buffer.memory_file = stream_source return self.capture_buffer return self.container -class MockFlushPart: - """Class to hold a wrapper function for check_flush_part.""" - - # Wrap this method with a preceding write so the BytesIO pointer moves - check_flush_part = SegmentBuffer.check_flush_part - - @classmethod - def wrapped_check_flush_part(cls, segment_buffer, packet): - """Wrap check_flush_part to also advance the memory_file pointer.""" - segment_buffer._memory_file.write(b"0") - return cls.check_flush_part(segment_buffer, packet) - - async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream(hass, STREAM_SOURCE, {}) @@ -230,10 +222,6 @@ async def async_decode_stream(hass, packets, py_av=None): with patch("av.open", new=py_av.open), patch( "homeassistant.components.stream.core.StreamOutput.put", side_effect=py_av.capture_buffer.capture_output_segment, - ), patch( - "homeassistant.components.stream.worker.SegmentBuffer.check_flush_part", - side_effect=MockFlushPart.wrapped_check_flush_part, - autospec=True, ): segment_buffer = SegmentBuffer(stream.outputs) stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) @@ -612,11 +600,7 @@ async def test_update_stream_source(hass): worker_wake.wait() return py_av.open(stream_source, args, kwargs) - with patch("av.open", new=blocking_open), patch( - "homeassistant.components.stream.worker.SegmentBuffer.check_flush_part", - side_effect=MockFlushPart.wrapped_check_flush_part, - autospec=True, - ): + with patch("av.open", new=blocking_open): stream.start() assert worker_open.wait(TIMEOUT) assert last_stream_source == STREAM_SOURCE From 648e6497181338a8ee4d846ce3b655692a52325a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 28 Jul 2021 10:26:14 +0200 Subject: [PATCH 686/818] Use SensorEntityDescription in AccuWeather integration (#53604) * Use SensorEntityDescription * Add missing type for entity_description * Use tuples instead of lists * Suggested change --- homeassistant/components/accuweather/const.py | 541 +++++++++--------- homeassistant/components/accuweather/model.py | 18 +- .../components/accuweather/sensor.py | 110 ++-- 3 files changed, 328 insertions(+), 341 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index aea394446ad..5802695afef 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -21,8 +21,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, CONCENTRATION_PARTS_PER_CUBIC_METER, DEVICE_CLASS_TEMPERATURE, LENGTH_FEET, @@ -38,16 +36,12 @@ from homeassistant.const import ( UV_INDEX, ) -from .model import SensorDescription +from .model import AccuWeatherSensorDescription API_IMPERIAL: Final = "Imperial" API_METRIC: Final = "Metric" ATTRIBUTION: Final = "Data provided by AccuWeather" -ATTR_ENABLED: Final = "enabled" ATTR_FORECAST: Final = "forecast" -ATTR_LABEL: Final = "label" -ATTR_UNIT_IMPERIAL: Final = "unit_imperial" -ATTR_UNIT_METRIC: Final = "unit_metric" CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." @@ -71,276 +65,263 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_WINDY: [32], } -FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = { - "CloudCoverDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-cloudy", - ATTR_LABEL: "Cloud Cover Day", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, - ATTR_ENABLED: False, - }, - "CloudCoverNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-cloudy", - ATTR_LABEL: "Cloud Cover Night", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, - ATTR_ENABLED: False, - }, - "Grass": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:grass", - ATTR_LABEL: "Grass Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED: False, - }, - "HoursOfSun": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-partly-cloudy", - ATTR_LABEL: "Hours Of Sun", - ATTR_UNIT_METRIC: TIME_HOURS, - ATTR_UNIT_IMPERIAL: TIME_HOURS, - ATTR_ENABLED: True, - }, - "Mold": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:blur", - ATTR_LABEL: "Mold Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED: False, - }, - "Ozone": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:vector-triangle", - ATTR_LABEL: "Ozone", - ATTR_UNIT_METRIC: None, - ATTR_UNIT_IMPERIAL: None, - ATTR_ENABLED: False, - }, - "Ragweed": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:sprout", - ATTR_LABEL: "Ragweed Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED: False, - }, - "RealFeelTemperatureMax": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Max", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: True, - }, - "RealFeelTemperatureMin": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Min", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: True, - }, - "RealFeelTemperatureShadeMax": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Shade Max", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - }, - "RealFeelTemperatureShadeMin": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Shade Min", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - }, - "ThunderstormProbabilityDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-lightning", - ATTR_LABEL: "Thunderstorm Probability Day", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, - ATTR_ENABLED: True, - }, - "ThunderstormProbabilityNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-lightning", - ATTR_LABEL: "Thunderstorm Probability Night", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, - ATTR_ENABLED: True, - }, - "Tree": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:tree-outline", - ATTR_LABEL: "Tree Pollen", - ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED: False, - }, - "UVIndex": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-sunny", - ATTR_LABEL: "UV Index", - ATTR_UNIT_METRIC: UV_INDEX, - ATTR_UNIT_IMPERIAL: UV_INDEX, - ATTR_ENABLED: True, - }, - "WindGustDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust Day", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, - ATTR_ENABLED: False, - }, - "WindGustNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust Night", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, - ATTR_ENABLED: False, - }, - "WindDay": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Day", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, - ATTR_ENABLED: True, - }, - "WindNight": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Night", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, - ATTR_ENABLED: True, - }, -} +FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( + AccuWeatherSensorDescription( + key="CloudCoverDay", + icon="mdi:weather-cloudy", + name="Cloud Cover Day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="CloudCoverNight", + icon="mdi:weather-cloudy", + name="Cloud Cover Night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Grass", + icon="mdi:grass", + name="Grass Pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="HoursOfSun", + icon="mdi:weather-partly-cloudy", + name="Hours Of Sun", + unit_metric=TIME_HOURS, + unit_imperial=TIME_HOURS, + ), + AccuWeatherSensorDescription( + key="Mold", + icon="mdi:blur", + name="Mold Pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ozone", + icon="mdi:vector-triangle", + name="Ozone", + unit_metric=None, + unit_imperial=None, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ragweed", + icon="mdi:sprout", + name="Ragweed Pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMax", + device_class=DEVICE_CLASS_TEMPERATURE, + name="RealFeel Temperature Max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMin", + device_class=DEVICE_CLASS_TEMPERATURE, + name="RealFeel Temperature Min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMax", + device_class=DEVICE_CLASS_TEMPERATURE, + name="RealFeel Temperature Shade Max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMin", + device_class=DEVICE_CLASS_TEMPERATURE, + name="RealFeel Temperature Shade Min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityDay", + icon="mdi:weather-lightning", + name="Thunderstorm Probability Day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityNight", + icon="mdi:weather-lightning", + name="Thunderstorm Probability Night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="Tree", + icon="mdi:tree-outline", + name="Tree Pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV Index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + ), + AccuWeatherSensorDescription( + key="WindGustDay", + icon="mdi:weather-windy", + name="Wind Gust Day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindGustNight", + icon="mdi:weather-windy", + name="Wind Gust Night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindDay", + icon="mdi:weather-windy", + name="Wind Day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), + AccuWeatherSensorDescription( + key="WindNight", + icon="mdi:weather-windy", + name="Wind Night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), +) -SENSOR_TYPES: Final[dict[str, SensorDescription]] = { - "ApparentTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Apparent Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "Ceiling": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-fog", - ATTR_LABEL: "Cloud Ceiling", - ATTR_UNIT_METRIC: LENGTH_METERS, - ATTR_UNIT_IMPERIAL: LENGTH_FEET, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "CloudCover": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-cloudy", - ATTR_LABEL: "Cloud Cover", - ATTR_UNIT_METRIC: PERCENTAGE, - ATTR_UNIT_IMPERIAL: PERCENTAGE, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "DewPoint": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Dew Point", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "RealFeelTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "RealFeelTemperatureShade": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "RealFeel Temperature Shade", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "Precipitation": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-rainy", - ATTR_LABEL: "Precipitation", - ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, - ATTR_UNIT_IMPERIAL: LENGTH_INCHES, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "PressureTendency": { - ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", - ATTR_ICON: "mdi:gauge", - ATTR_LABEL: "Pressure Tendency", - ATTR_UNIT_METRIC: None, - ATTR_UNIT_IMPERIAL: None, - ATTR_ENABLED: True, - }, - "UVIndex": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-sunny", - ATTR_LABEL: "UV Index", - ATTR_UNIT_METRIC: UV_INDEX, - ATTR_UNIT_IMPERIAL: UV_INDEX, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "WetBulbTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Wet Bulb Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "WindChillTemperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_LABEL: "Wind Chill Temperature", - ATTR_UNIT_METRIC: TEMP_CELSIUS, - ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "Wind": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, - ATTR_ENABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "WindGust": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_LABEL: "Wind Gust", - ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, - ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, - ATTR_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, -} +SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( + AccuWeatherSensorDescription( + key="ApparentTemperature", + device_class=DEVICE_CLASS_TEMPERATURE, + name="Apparent Temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Ceiling", + icon="mdi:weather-fog", + name="Cloud Ceiling", + unit_metric=LENGTH_METERS, + unit_imperial=LENGTH_FEET, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="CloudCover", + icon="mdi:weather-cloudy", + name="Cloud Cover", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="DewPoint", + device_class=DEVICE_CLASS_TEMPERATURE, + name="Dew Point", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperature", + device_class=DEVICE_CLASS_TEMPERATURE, + name="RealFeel Temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShade", + device_class=DEVICE_CLASS_TEMPERATURE, + name="RealFeel Temperature Shade", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Precipitation", + icon="mdi:weather-rainy", + name="Precipitation", + unit_metric=LENGTH_MILLIMETERS, + unit_imperial=LENGTH_INCHES, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="PressureTendency", + device_class="accuweather__pressure_tendency", + icon="mdi:gauge", + name="Pressure Tendency", + unit_metric=None, + unit_imperial=None, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV Index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WetBulbTemperature", + device_class=DEVICE_CLASS_TEMPERATURE, + name="Wet Bulb Temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindChillTemperature", + device_class=DEVICE_CLASS_TEMPERATURE, + name="Wind Chill Temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Wind", + icon="mdi:weather-windy", + name="Wind", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindGust", + icon="mdi:weather-windy", + name="Wind Gust", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), +) diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py index 2127629728b..e74a6d46057 100644 --- a/homeassistant/components/accuweather/model.py +++ b/homeassistant/components/accuweather/model.py @@ -1,16 +1,14 @@ """Type definitions for AccuWeather integration.""" from __future__ import annotations -from typing import TypedDict +from dataclasses import dataclass + +from homeassistant.components.sensor import SensorEntityDescription -class SensorDescription(TypedDict, total=False): - """Sensor description class.""" +@dataclass +class AccuWeatherSensorDescription(SensorEntityDescription): + """Class describing AccuWeather sensor entities.""" - device_class: str | None - icon: str | None - label: str - unit_metric: str | None - unit_imperial: str | None - enabled: bool - state_class: str | None + unit_metric: str | None = None + unit_imperial: str | None = None diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 3a774afe341..4a5af6054e1 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -3,15 +3,9 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_ICON, - CONF_NAME, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -21,11 +15,7 @@ from . import AccuWeatherDataUpdateCoordinator from .const import ( API_IMPERIAL, API_METRIC, - ATTR_ENABLED, ATTR_FORECAST, - ATTR_LABEL, - ATTR_UNIT_IMPERIAL, - ATTR_UNIT_METRIC, ATTRIBUTION, DOMAIN, FORECAST_SENSOR_TYPES, @@ -34,6 +24,7 @@ from .const import ( NAME, SENSOR_TYPES, ) +from .model import AccuWeatherSensorDescription PARALLEL_UPDATES = 1 @@ -47,17 +38,19 @@ async def async_setup_entry( coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors: list[AccuWeatherSensor] = [] - for sensor in SENSOR_TYPES: - sensors.append(AccuWeatherSensor(name, sensor, coordinator)) + for description in SENSOR_TYPES: + sensors.append(AccuWeatherSensor(name, coordinator, description)) if coordinator.forecast: - for sensor in FORECAST_SENSOR_TYPES: + for description in FORECAST_SENSOR_TYPES: for day in range(MAX_FORECAST_DAYS + 1): # Some air quality/allergy sensors are only available for certain # locations. - if sensor in coordinator.data[ATTR_FORECAST][0]: + if description.key in coordinator.data[ATTR_FORECAST][0]: sensors.append( - AccuWeatherSensor(name, sensor, coordinator, forecast_day=day) + AccuWeatherSensor( + name, coordinator, description, forecast_day=day + ) ) async_add_entities(sensors) @@ -67,69 +60,72 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): """Define an AccuWeather entity.""" coordinator: AccuWeatherDataUpdateCoordinator + entity_description: AccuWeatherSensorDescription def __init__( self, name: str, - kind: str, coordinator: AccuWeatherDataUpdateCoordinator, + description: AccuWeatherSensorDescription, forecast_day: int | None = None, ) -> None: """Initialize.""" super().__init__(coordinator) - self._sensor_data = _get_sensor_data(coordinator.data, forecast_day, kind) - if forecast_day is None: - self._description = SENSOR_TYPES[kind] - else: - self._description = FORECAST_SENSOR_TYPES[kind] - self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - self.kind = kind + self.entity_description = description + self._sensor_data = _get_sensor_data( + coordinator.data, forecast_day, description.key + ) self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} - self.forecast_day = forecast_day - self._attr_state_class = self._description.get(ATTR_STATE_CLASS) - self._attr_icon = self._description[ATTR_ICON] - self._attr_device_class = self._description[ATTR_DEVICE_CLASS] - self._attr_entity_registry_enabled_default = self._description[ATTR_ENABLED] - if self.forecast_day is not None: - self._attr_name = f"{name} {self._description[ATTR_LABEL]} {forecast_day}d" + if forecast_day is not None: + self._attr_name = f"{name} {description.name} {forecast_day}d" self._attr_unique_id = ( - f"{coordinator.location_key}-{kind}-{forecast_day}".lower() + f"{coordinator.location_key}-{description.key}-{forecast_day}".lower() ) else: - self._attr_name = f"{name} {self._description[ATTR_LABEL]}" - self._attr_unique_id = f"{coordinator.location_key}-{kind}".lower() + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = ( + f"{coordinator.location_key}-{description.key}".lower() + ) if coordinator.is_metric: - self._attr_unit_of_measurement = self._description[ATTR_UNIT_METRIC] + self._unit_system = API_METRIC + self._attr_unit_of_measurement = description.unit_metric else: - self._attr_unit_of_measurement = self._description[ATTR_UNIT_IMPERIAL] + self._unit_system = API_IMPERIAL + self._attr_unit_of_measurement = description.unit_imperial self._attr_device_info = { "identifiers": {(DOMAIN, coordinator.location_key)}, "name": NAME, "manufacturer": MANUFACTURER, "entry_type": "service", } + self.forecast_day = forecast_day @property def state(self) -> StateType: """Return the state.""" if self.forecast_day is not None: - if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE: + if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE: return cast(float, self._sensor_data["Value"]) - if self.kind == "UVIndex": + if self.entity_description.key == "UVIndex": return cast(int, self._sensor_data["Value"]) - if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]: + if self.entity_description.key in ("Grass", "Mold", "Ragweed", "Tree", "Ozone"): return cast(int, self._sensor_data["Value"]) - if self.kind == "Ceiling": + if self.entity_description.key == "Ceiling": return round(self._sensor_data[self._unit_system]["Value"]) - if self.kind == "PressureTendency": + if self.entity_description.key == "PressureTendency": return cast(str, self._sensor_data["LocalizedText"].lower()) - if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE: + if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE: return cast(float, self._sensor_data[self._unit_system]["Value"]) - if self.kind == "Precipitation": + if self.entity_description.key == "Precipitation": return cast(float, self._sensor_data[self._unit_system]["Value"]) - if self.kind in ["Wind", "WindGust"]: + if self.entity_description.key in ("Wind", "WindGust"): return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"]) - if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: + if self.entity_description.key in ( + "WindDay", + "WindNight", + "WindGustDay", + "WindGustNight", + ): return cast(StateType, self._sensor_data["Speed"]["Value"]) return cast(StateType, self._sensor_data) @@ -137,14 +133,26 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if self.forecast_day is not None: - if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: + if self.entity_description.key in ( + "WindDay", + "WindNight", + "WindGustDay", + "WindGustNight", + ): self._attrs["direction"] = self._sensor_data["Direction"]["English"] - elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: + elif self.entity_description.key in ( + "Grass", + "Mold", + "Ozone", + "Ragweed", + "Tree", + "UVIndex", + ): self._attrs["level"] = self._sensor_data["Category"] return self._attrs - if self.kind == "UVIndex": + if self.entity_description.key == "UVIndex": self._attrs["level"] = self.coordinator.data["UVIndexText"] - elif self.kind == "Precipitation": + elif self.entity_description.key == "Precipitation": self._attrs["type"] = self.coordinator.data["PrecipitationType"] return self._attrs @@ -152,7 +160,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): def _handle_coordinator_update(self) -> None: """Handle data update.""" self._sensor_data = _get_sensor_data( - self.coordinator.data, self.forecast_day, self.kind + self.coordinator.data, self.forecast_day, self.entity_description.key ) self.async_write_ha_state() From f3e7fb5798d71b01b25bba604251bfd68d4d198e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 28 Jul 2021 10:30:05 +0200 Subject: [PATCH 687/818] Pin pandas to 1.3.0 (#53607) Co-authored-by: Franck Nijhof --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 20dbd41e89b..80fc4359fc6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,3 +62,6 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 +# Temporary constraint on pandas, to unblock 2021.7 releases +# until we have fixed the wheels builds for newer versions. +pandas==1.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 57bb2b339aa..7dcc4f71fe8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -84,6 +84,9 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 +# Temporary constraint on pandas, to unblock 2021.7 releases +# until we have fixed the wheels builds for newer versions. +pandas==1.3.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 781015fb19297733849db0bf4db2063d9e2d0afe Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 28 Jul 2021 10:52:43 +0200 Subject: [PATCH 688/818] Xiaomi_Miio Humidifier rework (#52366) Co-authored-by: Martin Hjelmare Co-authored-by: Maciej Bieniek Co-authored-by: Teemu R. Co-authored-by: Franck Nijhof --- .coveragerc | 3 + .../components/xiaomi_miio/__init__.py | 79 ++- homeassistant/components/xiaomi_miio/const.py | 123 ++++- .../components/xiaomi_miio/device.py | 59 +++ homeassistant/components/xiaomi_miio/fan.py | 501 +----------------- .../components/xiaomi_miio/humidifier.py | 372 +++++++++++++ .../components/xiaomi_miio/number.py | 156 ++++++ .../components/xiaomi_miio/select.py | 189 +++++++ .../components/xiaomi_miio/sensor.py | 160 +++++- .../components/xiaomi_miio/switch.py | 264 ++++++++- 10 files changed, 1379 insertions(+), 527 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/humidifier.py create mode 100644 homeassistant/components/xiaomi_miio/number.py create mode 100644 homeassistant/components/xiaomi_miio/select.py diff --git a/.coveragerc b/.coveragerc index 96bde517482..c3516739798 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1205,8 +1205,11 @@ omit = homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/fan.py homeassistant/components/xiaomi_miio/gateway.py + homeassistant/components/xiaomi_miio/humidifier.py homeassistant/components/xiaomi_miio/light.py + homeassistant/components/xiaomi_miio/number.py homeassistant/components/xiaomi_miio/remote.py + homeassistant/components/xiaomi_miio/select.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py homeassistant/components/xiaomi_miio/vacuum.py diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 076aed4d30c..afc6783a0bb 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -2,12 +2,15 @@ from datetime import timedelta import logging +import async_timeout +from miio import AirHumidifier, AirHumidifierMiot, DeviceException from miio.gateway.gateway import GatewayException from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_AVAILABLE, @@ -17,8 +20,12 @@ from .const import ( CONF_MODEL, DOMAIN, KEY_COORDINATOR, + KEY_DEVICE, + KEY_MIGRATE_ENTITY_NAME, MODELS_AIR_MONITOR, MODELS_FAN, + MODELS_HUMIDIFIER, + MODELS_HUMIDIFIER_MIOT, MODELS_LIGHT, MODELS_SWITCH, MODELS_VACUUM, @@ -30,6 +37,7 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] FAN_PLATFORMS = ["fan"] +HUMIDIFIER_PLATFORMS = ["humidifier", "number", "select", "sensor", "switch"] LIGHT_PLATFORMS = ["light"] VACUUM_PLATFORMS = ["vacuum"] AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] @@ -51,6 +59,7 @@ async def async_setup_entry( ) +@callback def get_platforms(config_entry): """Return the platforms belonging to a config_entry.""" model = config_entry.data[CONF_MODEL] @@ -61,6 +70,8 @@ def get_platforms(config_entry): if flow_type == CONF_DEVICE: if model in MODELS_SWITCH: return SWITCH_PLATFORMS + if model in MODELS_HUMIDIFIER: + return HUMIDIFIER_PLATFORMS if model in MODELS_FAN: return FAN_PLATFORMS if model in MODELS_LIGHT: @@ -71,10 +82,70 @@ def get_platforms(config_entry): for air_monitor_model in MODELS_AIR_MONITOR: if model.startswith(air_monitor_model): return AIR_MONITOR_PLATFORMS - + _LOGGER.error( + "Unsupported device found! Please create an issue at " + "https://github.com/syssi/xiaomi_airpurifier/issues " + "and provide the following data: %s", + model, + ) return [] +async def async_create_miio_device_and_coordinator( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up a data coordinator and one miio device to service multiple entities.""" + model = entry.data[CONF_MODEL] + host = entry.data[CONF_HOST] + token = entry.data[CONF_TOKEN] + name = entry.title + device = None + migrate_entity_name = None + + if model not in MODELS_HUMIDIFIER: + return + if model in MODELS_HUMIDIFIER_MIOT: + device = AirHumidifierMiot(host, token) + else: + device = AirHumidifier(host, token, model=model) + + # Removing fan platform entity for humidifiers and cache the name and entity name for migration + entity_registry = er.async_get(hass) + entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) + if entity_id: + # This check is entities that have a platform migration only and should be removed in the future + migrate_entity_name = entity_registry.async_get(entity_id).name + entity_registry.async_remove(entity_id) + + async def async_update_data(): + """Fetch data from the device using async_add_executor_job.""" + try: + async with async_timeout.timeout(10): + return await hass.async_add_executor_job(device.status) + + except DeviceException as ex: + raise UpdateFailed(ex) from ex + + # Create update miio device and coordinator + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=60), + ) + hass.data[DOMAIN][entry.entry_id] = { + KEY_DEVICE: device, + KEY_COORDINATOR: coordinator, + } + if migrate_entity_name: + hass.data[DOMAIN][entry.entry_id][KEY_MIGRATE_ENTITY_NAME] = migrate_entity_name + + # Trigger first data fetch + await coordinator.async_config_entry_first_refresh() + + async def async_setup_gateway_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): @@ -130,7 +201,6 @@ async def async_setup_gateway_entry( coordinator = DataUpdateCoordinator( hass, _LOGGER, - # Name of the data. For logging purposes. name=name, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. @@ -155,6 +225,7 @@ async def async_setup_device_entry( ): """Set up the Xiaomi Miio device component from a config entry.""" platforms = get_platforms(entry) + await async_create_miio_device_and_coordinator(hass, entry) if not platforms: return False diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 27d0a34bf39..05499efb5d3 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -15,10 +15,17 @@ CONF_MANUAL = "manual" # Options flow CONF_CLOUD_SUBDEVICES = "cloud_subdevices" +# Keys KEY_COORDINATOR = "coordinator" +KEY_DEVICE = "device" +KEY_MIGRATE_ENTITY_NAME = "migrate_entity_name" +# Attributes ATTR_AVAILABLE = "available" +# Status +SUCCESS = ["ok"] + # Cloud SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"] DEFAULT_CLOUD_COUNTRY = "cn" @@ -70,10 +77,12 @@ MODELS_FAN_MIIO = [ MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H, + MODEL_AIRFRESH_VA2, +] +MODELS_HUMIDIFIER_MIIO = [ MODEL_AIRHUMIDIFIER_V1, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, - MODEL_AIRFRESH_VA2, ] # AirQuality Models @@ -108,7 +117,8 @@ MODELS_SWITCH = [ "chuangmi.plug.hmi205", "chuangmi.plug.hmi206", ] -MODELS_FAN = MODELS_FAN_MIIO + MODELS_HUMIDIFIER_MIOT + MODELS_PURIFIER_MIOT +MODELS_FAN = MODELS_FAN_MIIO + MODELS_PURIFIER_MIOT +MODELS_HUMIDIFIER = MODELS_HUMIDIFIER_MIOT + MODELS_HUMIDIFIER_MIIO MODELS_LIGHT = ( MODELS_LIGHT_EYECARE + MODELS_LIGHT_CEILING @@ -125,17 +135,29 @@ MODELS_AIR_MONITOR = [ ] MODELS_ALL_DEVICES = ( - MODELS_SWITCH + MODELS_VACUUM + MODELS_AIR_MONITOR + MODELS_FAN + MODELS_LIGHT + MODELS_SWITCH + + MODELS_VACUUM + + MODELS_AIR_MONITOR + + MODELS_FAN + + MODELS_HUMIDIFIER + + MODELS_LIGHT ) MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY -# Fan Services +# Fan/Humidifier Services SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" +SERVICE_SET_BUZZER = "set_buzzer" +SERVICE_SET_CLEAN_ON = "set_clean_on" +SERVICE_SET_CLEAN_OFF = "set_clean_off" +SERVICE_SET_CLEAN = "set_clean" SERVICE_SET_FAN_LED_ON = "fan_set_led_on" SERVICE_SET_FAN_LED_OFF = "fan_set_led_off" +SERVICE_SET_FAN_LED = "fan_set_led" +SERVICE_SET_LED_BRIGHTNESS = "set_led_brightness" SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on" SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off" +SERVICE_SET_CHILD_LOCK = "set_child_lock" SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness" SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level" SERVICE_SET_FAN_LEVEL = "fan_set_fan_level" @@ -149,6 +171,7 @@ SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" SERVICE_SET_DRY_ON = "fan_set_dry_on" SERVICE_SET_DRY_OFF = "fan_set_dry_off" +SERVICE_SET_DRY = "set_dry" SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed" # Light Services @@ -180,3 +203,95 @@ SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" SERVICE_CLEAN_SEGMENT = "vacuum_clean_segment" SERVICE_CLEAN_ZONE = "vacuum_clean_zone" SERVICE_GOTO = "vacuum_goto" + +# Features +FEATURE_SET_BUZZER = 1 +FEATURE_SET_LED = 2 +FEATURE_SET_CHILD_LOCK = 4 +FEATURE_SET_LED_BRIGHTNESS = 8 +FEATURE_SET_FAVORITE_LEVEL = 16 +FEATURE_SET_AUTO_DETECT = 32 +FEATURE_SET_LEARN_MODE = 64 +FEATURE_SET_VOLUME = 128 +FEATURE_RESET_FILTER = 256 +FEATURE_SET_EXTRA_FEATURES = 512 +FEATURE_SET_TARGET_HUMIDITY = 1024 +FEATURE_SET_DRY = 2048 +FEATURE_SET_FAN_LEVEL = 4096 +FEATURE_SET_MOTOR_SPEED = 8192 +FEATURE_SET_CLEAN = 16384 + +FEATURE_FLAGS_AIRPURIFIER = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_SET_FAVORITE_LEVEL + | FEATURE_SET_LEARN_MODE + | FEATURE_RESET_FILTER + | FEATURE_SET_EXTRA_FEATURES +) + +FEATURE_FLAGS_AIRPURIFIER_PRO = ( + FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_FAVORITE_LEVEL + | FEATURE_SET_AUTO_DETECT + | FEATURE_SET_VOLUME +) + +FEATURE_FLAGS_AIRPURIFIER_PRO_V7 = ( + FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_FAVORITE_LEVEL + | FEATURE_SET_VOLUME +) + +FEATURE_FLAGS_AIRPURIFIER_2S = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_FAVORITE_LEVEL +) + +FEATURE_FLAGS_AIRPURIFIER_3 = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_FAVORITE_LEVEL + | FEATURE_SET_FAN_LEVEL + | FEATURE_SET_LED_BRIGHTNESS +) + +FEATURE_FLAGS_AIRPURIFIER_V3 = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED +) + +FEATURE_FLAGS_AIRHUMIDIFIER = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_SET_TARGET_HUMIDITY +) + +FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY + +FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_SET_TARGET_HUMIDITY + | FEATURE_SET_DRY + | FEATURE_SET_MOTOR_SPEED + | FEATURE_SET_CLEAN +) + +FEATURE_FLAGS_AIRFRESH = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_RESET_FILTER + | FEATURE_SET_EXTRA_FEATURES +) diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 081b910efdb..f8402138f21 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -1,4 +1,5 @@ """Code to handle a Xiaomi Device.""" +from functools import partial import logging from construct.core import ChecksumError @@ -7,6 +8,7 @@ from miio import Device, DeviceException from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_MAC, CONF_MODEL, DOMAIN @@ -73,6 +75,7 @@ class XiaomiMiioEntity(Entity): self._device_id = entry.unique_id self._unique_id = unique_id self._name = name + self._available = None @property def unique_id(self): @@ -98,3 +101,59 @@ class XiaomiMiioEntity(Entity): device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} return device_info + + +class XiaomiCoordinatedMiioEntity(CoordinatorEntity): + """Representation of a base a coordinated Xiaomi Miio Entity.""" + + def __init__(self, name, device, entry, unique_id, coordinator): + """Initialize the coordinated Xiaomi Miio Device.""" + super().__init__(coordinator) + self._device = device + self._model = entry.data[CONF_MODEL] + self._mac = entry.data[CONF_MAC] + self._device_id = entry.unique_id + self._device_name = entry.title + self._unique_id = unique_id + self._name = name + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def device_info(self): + """Return the device info.""" + device_info = { + "identifiers": {(DOMAIN, self._device_id)}, + "manufacturer": "Xiaomi", + "name": self._device_name, + "model": self._model, + } + + if self._mac is not None: + device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} + + return device_info + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a miio device command handling error messages.""" + try: + result = await self.hass.async_add_executor_job( + partial(func, *args, **kwargs) + ) + + _LOGGER.debug("Response received from miio device: %s", result) + + return True + except DeviceException as exc: + if self.available: + _LOGGER.error(mask_error, exc) + + return False diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index bdef9517cca..c58d9ad0c66 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -5,27 +5,11 @@ from functools import partial import logging import math -from miio import ( - AirFresh, - AirHumidifier, - AirHumidifierMiot, - AirPurifier, - AirPurifierMiot, - DeviceException, -) +from miio import AirFresh, AirPurifier, AirPurifierMiot, DeviceException from miio.airfresh import ( LedBrightness as AirfreshLedBrightness, OperationMode as AirfreshOperationMode, ) -from miio.airhumidifier import ( - LedBrightness as AirhumidifierLedBrightness, - OperationMode as AirhumidifierOperationMode, -) -from miio.airhumidifier_miot import ( - LedBrightness as AirhumidifierMiotLedBrightness, - OperationMode as AirhumidifierMiotOperationMode, - PressedButton as AirhumidifierPressedButton, -) from miio.airpurifier import ( LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, @@ -38,9 +22,6 @@ import voluptuous as vol from homeassistant.components.fan import ( PLATFORM_SCHEMA, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, @@ -64,16 +45,23 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, - MODEL_AIRHUMIDIFIER_CA1, - MODEL_AIRHUMIDIFIER_CA4, - MODEL_AIRHUMIDIFIER_CB1, + FEATURE_RESET_FILTER, + FEATURE_SET_AUTO_DETECT, + FEATURE_SET_BUZZER, + FEATURE_SET_CHILD_LOCK, + FEATURE_SET_EXTRA_FEATURES, + FEATURE_SET_FAN_LEVEL, + FEATURE_SET_FAVORITE_LEVEL, + FEATURE_SET_LEARN_MODE, + FEATURE_SET_LED, + FEATURE_SET_LED_BRIGHTNESS, + FEATURE_SET_VOLUME, MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V3, MODELS_FAN, - MODELS_HUMIDIFIER_MIOT, MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_AUTO_DETECT_OFF, @@ -82,8 +70,6 @@ from .const import ( SERVICE_SET_BUZZER_ON, SERVICE_SET_CHILD_LOCK_OFF, SERVICE_SET_CHILD_LOCK_ON, - SERVICE_SET_DRY_OFF, - SERVICE_SET_DRY_ON, SERVICE_SET_EXTRA_FEATURES, SERVICE_SET_FAN_LED_OFF, SERVICE_SET_FAN_LED_ON, @@ -92,9 +78,8 @@ from .const import ( SERVICE_SET_LEARN_MODE_OFF, SERVICE_SET_LEARN_MODE_ON, SERVICE_SET_LED_BRIGHTNESS, - SERVICE_SET_MOTOR_SPEED, - SERVICE_SET_TARGET_HUMIDITY, SERVICE_SET_VOLUME, + SUCCESS, ) from .device import XiaomiMiioEntity @@ -150,20 +135,6 @@ ATTR_VOLUME = "volume" ATTR_USE_TIME = "use_time" ATTR_BUTTON_PRESSED = "button_pressed" -# Air Humidifier -ATTR_TARGET_HUMIDITY = "target_humidity" -ATTR_TRANS_LEVEL = "trans_level" -ATTR_HARDWARE_VERSION = "hardware_version" - -# Air Humidifier CA -# ATTR_MOTOR_SPEED = "motor_speed" -ATTR_DEPTH = "depth" -ATTR_DRY = "dry" - -# Air Humidifier CA4 -ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" -ATTR_FAHRENHEIT = "fahrenheit" - # Air Fresh ATTR_CO2 = "co2" @@ -283,41 +254,6 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { ATTR_BUTTON_PRESSED: "button_pressed", } -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON = { - ATTR_TEMPERATURE: "temperature", - ATTR_HUMIDITY: "humidity", - ATTR_MODE: "mode", - ATTR_BUZZER: "buzzer", - ATTR_CHILD_LOCK: "child_lock", - ATTR_TARGET_HUMIDITY: "target_humidity", - ATTR_LED_BRIGHTNESS: "led_brightness", - ATTR_USE_TIME: "use_time", -} - -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { - **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_TRANS_LEVEL: "trans_level", - ATTR_BUTTON_PRESSED: "button_pressed", - ATTR_HARDWARE_VERSION: "hardware_version", -} - -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = { - **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_MOTOR_SPEED: "motor_speed", - ATTR_DEPTH: "depth", - ATTR_DRY: "dry", - ATTR_HARDWARE_VERSION: "hardware_version", -} - -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 = { - **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", - ATTR_BUTTON_PRESSED: "button_pressed", - ATTR_DRY: "dry", - ATTR_FAHRENHEIT: "fahrenheit", - ATTR_MOTOR_SPEED: "motor_speed", -} - AVAILABLE_ATTRIBUTES_AIRFRESH = { ATTR_TEMPERATURE: "temperature", ATTR_AIR_QUALITY_INDEX: "aqi", @@ -365,25 +301,6 @@ PRESET_MODES_AIRPURIFIER_V3 = [ ] OPERATION_MODES_AIRFRESH = ["Auto", "Silent", "Interval", "Low", "Middle", "Strong"] PRESET_MODES_AIRFRESH = ["Auto", "Interval"] -PRESET_MODES_AIRHUMIDIFIER = ["Auto"] -PRESET_MODES_AIRHUMIDIFIER_CA4 = ["Auto"] - -SUCCESS = ["ok"] - -FEATURE_SET_BUZZER = 1 -FEATURE_SET_LED = 2 -FEATURE_SET_CHILD_LOCK = 4 -FEATURE_SET_LED_BRIGHTNESS = 8 -FEATURE_SET_FAVORITE_LEVEL = 16 -FEATURE_SET_AUTO_DETECT = 32 -FEATURE_SET_LEARN_MODE = 64 -FEATURE_SET_VOLUME = 128 -FEATURE_RESET_FILTER = 256 -FEATURE_SET_EXTRA_FEATURES = 512 -FEATURE_SET_TARGET_HUMIDITY = 1024 -FEATURE_SET_DRY = 2048 -FEATURE_SET_FAN_LEVEL = 4096 -FEATURE_SET_MOTOR_SPEED = 8192 FEATURE_FLAGS_AIRPURIFIER = ( FEATURE_SET_BUZZER @@ -431,25 +348,6 @@ FEATURE_FLAGS_AIRPURIFIER_V3 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED ) -FEATURE_FLAGS_AIRHUMIDIFIER = ( - FEATURE_SET_BUZZER - | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS - | FEATURE_SET_TARGET_HUMIDITY -) - -FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY - -FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( - FEATURE_SET_BUZZER - | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED_BRIGHTNESS - | FEATURE_SET_TARGET_HUMIDITY - | FEATURE_SET_DRY - | FEATURE_SET_MOTOR_SPEED -) - FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK @@ -481,22 +379,6 @@ SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend( {vol.Required(ATTR_FEATURES): cv.positive_int} ) -SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_HUMIDITY): vol.All( - vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]) - ) - } -) - -SERVICE_SCHEMA_MOTOR_SPEED = AIRPURIFIER_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MOTOR_SPEED): vol.All( - vol.Coerce(int), vol.Clamp(min=200, max=2000) - ) - } -) - SERVICE_TO_METHOD = { SERVICE_SET_BUZZER_ON: {"method": "async_set_buzzer_on"}, SERVICE_SET_BUZZER_OFF: {"method": "async_set_buzzer_off"}, @@ -526,16 +408,6 @@ SERVICE_TO_METHOD = { "method": "async_set_extra_features", "schema": SERVICE_SCHEMA_EXTRA_FEATURES, }, - SERVICE_SET_TARGET_HUMIDITY: { - "method": "async_set_target_humidity", - "schema": SERVICE_SCHEMA_TARGET_HUMIDITY, - }, - SERVICE_SET_DRY_ON: {"method": "async_set_dry_on"}, - SERVICE_SET_DRY_OFF: {"method": "async_set_dry_off"}, - SERVICE_SET_MOTOR_SPEED: { - "method": "async_set_motor_speed", - "schema": SERVICE_SCHEMA_MOTOR_SPEED, - }, } @@ -578,14 +450,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif model.startswith("zhimi.airpurifier."): air_purifier = AirPurifier(host, token) entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id) - elif model in MODELS_HUMIDIFIER_MIOT: - air_humidifier = AirHumidifierMiot(host, token) - entity = XiaomiAirHumidifierMiot( - name, air_humidifier, config_entry, unique_id - ) - elif model.startswith("zhimi.humidifier."): - air_humidifier = AirHumidifier(host, token, model=model) - entity = XiaomiAirHumidifier(name, air_humidifier, config_entry, unique_id) elif model.startswith("zhimi.airfresh."): air_fresh = AirFresh(host, token) entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id) @@ -1247,345 +1111,6 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): ) -class XiaomiAirHumidifier(XiaomiGenericDevice): - """Representation of a Xiaomi Air Humidifier.""" - - SPEED_MODE_MAPPING = { - 1: AirhumidifierOperationMode.Silent, - 2: AirhumidifierOperationMode.Medium, - 3: AirhumidifierOperationMode.High, - 4: AirhumidifierOperationMode.Strong, - } - - REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - - PRESET_MODE_MAPPING = { - "Auto": AirhumidifierOperationMode.Auto, - } - - def __init__(self, name, device, entry, unique_id): - """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id) - self._percentage = None - self._preset_mode = None - self._supported_features = SUPPORT_SET_SPEED - self._preset_modes = [] - if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [ - mode.name - for mode in AirhumidifierOperationMode - if mode is not AirhumidifierOperationMode.Strong - ] - self._supported_features |= SUPPORT_PRESET_MODE - self._preset_modes = PRESET_MODES_AIRHUMIDIFIER - self._speed_count = 3 - elif self._model in [MODEL_AIRHUMIDIFIER_CA4]: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4 - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - self._supported_features |= SUPPORT_PRESET_MODE - self._preset_modes = PRESET_MODES_AIRHUMIDIFIER - self._speed_count = 3 - else: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [ - mode.name - for mode in AirhumidifierOperationMode - if mode is not AirhumidifierOperationMode.Auto - ] - self._supported_features |= SUPPORT_PRESET_MODE - self._preset_modes = PRESET_MODES_AIRHUMIDIFIER - self._speed_count = 4 - - self._state_attrs.update( - {attribute: None for attribute in self._available_attributes} - ) - - async def async_update(self): - """Fetch state from the device.""" - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job(self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - { - key: self._extract_value_from_attribute(state, value) - for key, value in self._available_attributes.items() - } - ) - - except DeviceException as ex: - if self._available: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) - - @property - def preset_mode(self): - """Get the active preset mode.""" - if self._state: - preset_mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name - return preset_mode if preset_mode in self._preset_modes else None - - return None - - @property - def percentage(self): - """Return the current percentage based speed.""" - if self._state: - mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]) - if mode in self.REVERSE_SPEED_MODE_MAPPING: - return ranged_value_to_percentage( - (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] - ) - - return None - - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name - - return None - - async def async_set_percentage(self, percentage: int) -> None: - """Set the percentage of the fan. - - This method is a coroutine. - """ - speed_mode = math.ceil( - percentage_to_ranged_value((1, self._speed_count), percentage) - ) - if speed_mode: - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirhumidifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), - ) - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode of the fan. - - This method is a coroutine. - """ - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - self.PRESET_MODE_MAPPING[preset_mode], - ) - - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirhumidifierOperationMode[speed.title()], - ) - - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirhumidifierLedBrightness(brightness), - ) - - async def async_set_target_humidity(self, humidity: int = 40): - """Set the target humidity.""" - if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0: - return - - await self._try_command( - "Setting the target humidity of the miio device failed.", - self._device.set_target_humidity, - humidity, - ) - - async def async_set_dry_on(self): - """Turn the dry mode on.""" - if self._device_features & FEATURE_SET_DRY == 0: - return - - await self._try_command( - "Turning the dry mode of the miio device off failed.", - self._device.set_dry, - True, - ) - - async def async_set_dry_off(self): - """Turn the dry mode off.""" - if self._device_features & FEATURE_SET_DRY == 0: - return - - await self._try_command( - "Turning the dry mode of the miio device off failed.", - self._device.set_dry, - False, - ) - - -class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): - """Representation of a Xiaomi Air Humidifier (MiOT protocol).""" - - PRESET_MODE_MAPPING = { - AirhumidifierMiotOperationMode.Auto: "Auto", - } - - REVERSE_PRESET_MODE_MAPPING = {v: k for k, v in PRESET_MODE_MAPPING.items()} - - SPEED_MAPPING = { - AirhumidifierMiotOperationMode.Low: SPEED_LOW, - AirhumidifierMiotOperationMode.Mid: SPEED_MEDIUM, - AirhumidifierMiotOperationMode.High: SPEED_HIGH, - } - - REVERSE_SPEED_MAPPING = {v: k for k, v in SPEED_MAPPING.items()} - - SPEEDS = [ - AirhumidifierMiotOperationMode.Low, - AirhumidifierMiotOperationMode.Mid, - AirhumidifierMiotOperationMode.High, - ] - - # the speed attribute is deprecated, support will end with release 2021.7 - # it is added here for compatibility - @property - def speed(self): - """Return current legacy speed.""" - if ( - self.state - and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) - in self.SPEED_MAPPING - ): - return self.SPEED_MAPPING[ - AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) - ] - return None - - @property - def percentage(self): - """Return the current percentage based speed.""" - if ( - self.state - and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) - in self.SPEEDS - ): - return ranged_value_to_percentage( - (1, self.speed_count), self._state_attrs[ATTR_MODE] - ) - - return None - - @property - def preset_mode(self): - """Return the current preset_mode.""" - if self._state: - mode = self.PRESET_MODE_MAPPING.get( - AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) - ) - if mode in self._preset_modes: - return mode - - return None - - @property - def button_pressed(self): - """Return the last button pressed.""" - if self._state: - return AirhumidifierPressedButton( - self._state_attrs[ATTR_BUTTON_PRESSED] - ).name - - return None - - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Override for set async_set_speed of the super() class.""" - if speed and speed in self.REVERSE_SPEED_MAPPING: - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - self.REVERSE_SPEED_MAPPING[speed], - ) - - async def async_set_percentage(self, percentage: int) -> None: - """Set the percentage of the fan. - - This method is a coroutine. - """ - mode = math.ceil(percentage_to_ranged_value((1, 3), percentage)) - if mode: - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirhumidifierMiotOperationMode(mode), - ) - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode of the fan. - - This method is a coroutine. - """ - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - self.REVERSE_PRESET_MODE_MAPPING[preset_mode], - ) - - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirhumidifierMiotLedBrightness(brightness), - ) - - async def async_set_motor_speed(self, motor_speed: int = 400): - """Set the target motor speed.""" - if self._device_features & FEATURE_SET_MOTOR_SPEED == 0: - return - - await self._try_command( - "Setting the target motor speed of the miio device failed.", - self._device.set_speed, - motor_speed, - ) - - class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py new file mode 100644 index 00000000000..1535a8772ab --- /dev/null +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -0,0 +1,372 @@ +"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity.""" +from enum import Enum +import logging +import math + +from miio.airhumidifier import OperationMode as AirhumidifierOperationMode +from miio.airhumidifier_miot import OperationMode as AirhumidifierMiotOperationMode + +from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier.const import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_HUMIDIFIER, + SUPPORT_MODES, +) +from homeassistant.const import ATTR_MODE, CONF_HOST, CONF_TOKEN +from homeassistant.core import callback +from homeassistant.util.percentage import percentage_to_ranged_value + +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, + KEY_MIGRATE_ENTITY_NAME, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, + MODEL_AIRHUMIDIFIER_CB1, + MODELS_HUMIDIFIER_MIOT, +) +from .device import XiaomiCoordinatedMiioEntity + +_LOGGER = logging.getLogger(__name__) + +# Air Humidifier +ATTR_TARGET_HUMIDITY = "target_humidity" + +AVAILABLE_ATTRIBUTES = { + ATTR_MODE: "mode", + ATTR_TARGET_HUMIDITY: "target_humidity", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Humidifier from a config entry.""" + if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + return + + entities = [] + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: + name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] + else: + name = config_entry.title + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + + if model in MODELS_HUMIDIFIER_MIOT: + air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + entity = XiaomiAirHumidifierMiot( + name, + air_humidifier, + config_entry, + unique_id, + coordinator, + ) + else: + air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + entity = XiaomiAirHumidifier( + name, + air_humidifier, + config_entry, + unique_id, + coordinator, + ) + + entities.append(entity) + + async_add_entities(entities) + + +class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): + """Representation of a generic Xiaomi humidifier device.""" + + _attr_device_class = DEVICE_CLASS_HUMIDIFIER + _attr_supported_features = SUPPORT_MODES + + def __init__(self, name, device, entry, unique_id, coordinator): + """Initialize the generic Xiaomi device.""" + super().__init__(name, device, entry, unique_id, coordinator=coordinator) + + self._state = None + self._attributes = {} + self._available_modes = [] + self._mode = None + self._min_humidity = DEFAULT_MIN_HUMIDITY + self._max_humidity = DEFAULT_MAX_HUMIDITY + self._humidity_steps = 100 + self._target_humidity = None + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + @property + def available_modes(self) -> list: + """Get the list of available modes.""" + return self._available_modes + + @property + def mode(self): + """Get the current mode.""" + return self._mode + + @property + def min_humidity(self): + """Return the minimum target humidity.""" + return self._min_humidity + + @property + def max_humidity(self): + """Return the maximum target humidity.""" + return self._max_humidity + + async def async_turn_on( + self, + **kwargs, + ) -> None: + """Turn the device on.""" + result = await self._try_command( + "Turning the miio device on failed.", self._device.on + ) + if result: + self._state = True + + async def async_turn_off(self, **kwargs) -> None: + """Turn the device off.""" + result = await self._try_command( + "Turning the miio device off failed.", self._device.off + ) + + if result: + self._state = False + + def translate_humidity(self, humidity): + """Translate the target humidity to the first valid step.""" + return ( + math.ceil(percentage_to_ranged_value((1, self._humidity_steps), humidity)) + * 100 + / self._humidity_steps + if 0 < humidity <= 100 + else None + ) + + +class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): + """Representation of a Xiaomi Air Humidifier.""" + + def __init__(self, name, device, entry, unique_id, coordinator): + """Initialize the plug switch.""" + super().__init__(name, device, entry, unique_id, coordinator) + if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: + self._available_modes = [] + self._available_modes = [ + mode.name + for mode in AirhumidifierOperationMode + if mode is not AirhumidifierOperationMode.Strong + ] + self._min_humidity = 30 + self._max_humidity = 80 + self._humidity_steps = 10 + elif self._model in [MODEL_AIRHUMIDIFIER_CA4]: + self._available_modes = [ + mode.name for mode in AirhumidifierMiotOperationMode + ] + self._min_humidity = 30 + self._max_humidity = 80 + self._humidity_steps = 100 + else: + self._available_modes = [ + mode.name + for mode in AirhumidifierOperationMode + if mode is not AirhumidifierOperationMode.Auto + ] + self._min_humidity = 30 + self._max_humidity = 80 + self._humidity_steps = 10 + + self._state = self.coordinator.data.is_on + self._attributes.update( + { + key: self._extract_value_from_attribute(self.coordinator.data, value) + for key, value in AVAILABLE_ATTRIBUTES.items() + } + ) + self._target_humidity = self._attributes[ATTR_TARGET_HUMIDITY] + self._mode = self._attributes[ATTR_MODE] + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._state = self.coordinator.data.is_on + self._attributes.update( + { + key: self._extract_value_from_attribute(self.coordinator.data, value) + for key, value in AVAILABLE_ATTRIBUTES.items() + } + ) + self._target_humidity = self._attributes[ATTR_TARGET_HUMIDITY] + self._mode = self._attributes[ATTR_MODE] + self.async_write_ha_state() + + @property + def mode(self): + """Return the current mode.""" + return AirhumidifierOperationMode(self._mode).name + + @property + def target_humidity(self): + """Return the target humidity.""" + return ( + self._target_humidity + if self._mode == AirhumidifierOperationMode.Auto.name + or AirhumidifierOperationMode.Auto.name not in self.available_modes + else None + ) + + async def async_set_humidity(self, humidity: int) -> None: + """Set the target humidity of the humidifier and set the mode to auto.""" + target_humidity = self.translate_humidity(humidity) + if not target_humidity: + return + + _LOGGER.debug("Setting the target humidity to: %s", target_humidity) + if await self._try_command( + "Setting target humidity of the miio device failed.", + self._device.set_target_humidity, + target_humidity, + ): + self._target_humidity = target_humidity + if ( + self.supported_features & SUPPORT_MODES == 0 + or AirhumidifierOperationMode(self._attributes[ATTR_MODE]) + == AirhumidifierOperationMode.Auto + or AirhumidifierOperationMode.Auto.name not in self.available_modes + ): + self.async_write_ha_state() + return + _LOGGER.debug("Setting the operation mode to: Auto") + if await self._try_command( + "Setting operation mode of the miio device to MODE_AUTO failed.", + self._device.set_mode, + AirhumidifierOperationMode.Auto, + ): + self._mode = AirhumidifierOperationMode.Auto.name + self.async_write_ha_state() + + async def async_set_mode(self, mode: str) -> None: + """Set the mode of the humidifier.""" + if self.supported_features & SUPPORT_MODES == 0 or not mode: + return + + if mode not in self.available_modes: + _LOGGER.warning("Mode %s is not a valid operation mode", mode) + return + + _LOGGER.debug("Setting the operation mode to: %s", mode) + if await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + AirhumidifierOperationMode[mode.title()], + ): + self._mode = mode.title() + self.async_write_ha_state() + + +class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): + """Representation of a Xiaomi Air Humidifier (MiOT protocol).""" + + MODE_MAPPING = { + AirhumidifierMiotOperationMode.Auto: "Auto", + AirhumidifierMiotOperationMode.Low: "Low", + AirhumidifierMiotOperationMode.Mid: "Mid", + AirhumidifierMiotOperationMode.High: "High", + } + + REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()} + + @property + def mode(self): + """Return the current mode.""" + return AirhumidifierMiotOperationMode(self._mode).name + + @property + def target_humidity(self): + """Return the target humidity.""" + if self._state: + return ( + self._target_humidity + if AirhumidifierMiotOperationMode(self._mode) + == AirhumidifierMiotOperationMode.Auto + else None + ) + return None + + async def async_set_humidity(self, humidity: int) -> None: + """Set the target humidity of the humidifier and set the mode to auto.""" + target_humidity = self.translate_humidity(humidity) + if not target_humidity: + return + + _LOGGER.debug("Setting the humidity to: %s", target_humidity) + if await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_target_humidity, + target_humidity, + ): + self._target_humidity = target_humidity + if ( + self.supported_features & SUPPORT_MODES == 0 + or AirhumidifierMiotOperationMode(self._attributes[ATTR_MODE]) + == AirhumidifierMiotOperationMode.Auto + ): + self.async_write_ha_state() + return + _LOGGER.debug("Setting the operation mode to: Auto") + if await self._try_command( + "Setting operation mode of the miio device to MODE_AUTO failed.", + self._device.set_mode, + AirhumidifierMiotOperationMode.Auto, + ): + self._mode = 0 + self.async_write_ha_state() + + async def async_set_mode(self, mode: str) -> None: + """Set the mode of the fan.""" + if self.supported_features & SUPPORT_MODES == 0 or not mode: + return + + if mode not in self.REVERSE_MODE_MAPPING: + _LOGGER.warning("Mode %s is not a valid operation mode", mode) + return + + _LOGGER.debug("Setting the operation mode to: %s", mode) + if self._state: + if await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.REVERSE_MODE_MAPPING[mode], + ): + self._mode = self.REVERSE_MODE_MAPPING[mode].value + self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py new file mode 100644 index 00000000000..f81f46967ab --- /dev/null +++ b/homeassistant/components/xiaomi_miio/number.py @@ -0,0 +1,156 @@ +"""Motor speed support for Xiaomi Mi Air Humidifier.""" +from dataclasses import dataclass +from enum import Enum +import logging + +from homeassistant.components.number import NumberEntity +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.core import callback + +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, + DOMAIN, + FEATURE_SET_MOTOR_SPEED, + KEY_COORDINATOR, + KEY_DEVICE, + KEY_MIGRATE_ENTITY_NAME, + MODEL_AIRHUMIDIFIER_CA4, +) +from .device import XiaomiCoordinatedMiioEntity + +_LOGGER = logging.getLogger(__name__) + +ATTR_MOTOR_SPEED = "motor_speed" + + +@dataclass +class NumberType: + """Class that holds device specific info for a xiaomi aqara or humidifier number controller types.""" + + name: str = None + short_name: str = None + unit_of_measurement: str = None + icon: str = None + device_class: str = None + min: float = None + max: float = None + step: float = None + available_with_device_off: bool = True + + +NUMBER_TYPES = { + FEATURE_SET_MOTOR_SPEED: NumberType( + name="Motor Speed", + icon="mdi:fast-forward-outline", + short_name=ATTR_MOTOR_SPEED, + unit_of_measurement="rpm", + min=200, + max=2000, + step=10, + available_with_device_off=False, + ), +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Selectors from a config entry.""" + entities = [] + if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + return + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + model = config_entry.data[CONF_MODEL] + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: + name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] + else: + name = config_entry.title + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + if model not in [MODEL_AIRHUMIDIFIER_CA4]: + return + + for number in NUMBER_TYPES.values(): + entities.append( + XiaomiAirHumidifierNumber( + f"{name} {number.name}", + device, + config_entry, + f"{number.short_name}_{config_entry.unique_id}", + number, + coordinator, + ) + ) + + async_add_entities(entities) + + +class XiaomiAirHumidifierNumber(XiaomiCoordinatedMiioEntity, NumberEntity): + """Representation of a generic Xiaomi attribute selector.""" + + def __init__(self, name, device, entry, unique_id, number, coordinator): + """Initialize the generic Xiaomi attribute selector.""" + super().__init__(name, device, entry, unique_id, coordinator) + self._attr_icon = number.icon + self._attr_unit_of_measurement = number.unit_of_measurement + self._attr_min_value = number.min + self._attr_max_value = number.max + self._attr_step = number.step + self._controller = number + self._attr_value = self._extract_value_from_attribute( + self.coordinator.data, self._controller.short_name + ) + + @property + def available(self): + """Return the number controller availability.""" + if ( + super().available + and not self.coordinator.data.is_on + and not self._controller.available_with_device_off + ): + return False + return super().available + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + async def async_set_value(self, value): + """Set an option of the miio device.""" + if ( + self.min_value + and value < self.min_value + or self.max_value + and value > self.max_value + ): + raise ValueError( + f"Value {value} not a valid {self.name} within the range {self.min_value} - {self.max_value}" + ) + if await self.async_set_motor_speed(value): + self._attr_value = value + self.async_write_ha_state() + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + # On state change the device doesn't provide the new state immediately. + self._attr_value = self._extract_value_from_attribute( + self.coordinator.data, self._controller.short_name + ) + self.async_write_ha_state() + + async def async_set_motor_speed(self, motor_speed: int = 400): + """Set the target motor speed.""" + return await self._try_command( + "Setting the target motor speed of the miio device failed.", + self._device.set_speed, + motor_speed, + ) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py new file mode 100644 index 00000000000..40a236aab6a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/select.py @@ -0,0 +1,189 @@ +"""Support led_brightness for Mi Air Humidifier.""" +from dataclasses import dataclass +from enum import Enum +import logging + +from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness +from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness + +from homeassistant.components.select import SelectEntity +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.core import callback + +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, + DOMAIN, + FEATURE_SET_LED_BRIGHTNESS, + KEY_COORDINATOR, + KEY_DEVICE, + KEY_MIGRATE_ENTITY_NAME, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, + MODEL_AIRHUMIDIFIER_CB1, + MODELS_HUMIDIFIER, + SERVICE_SET_LED_BRIGHTNESS, +) +from .device import XiaomiCoordinatedMiioEntity + +_LOGGER = logging.getLogger(__name__) + +ATTR_LED_BRIGHTNESS = "led_brightness" + + +LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2} +LED_BRIGHTNESS_MAP_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} +LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()} +LED_BRIGHTNESS_REVERSE_MAP_MIOT = { + val: key for key, val in LED_BRIGHTNESS_MAP_MIOT.items() +} + + +@dataclass +class SelectorType: + """Class that holds device specific info for a xiaomi aqara or humidifier selectors.""" + + name: str = None + icon: str = None + short_name: str = None + options: list = None + service: str = None + + +SELECTOR_TYPES = { + FEATURE_SET_LED_BRIGHTNESS: SelectorType( + name="Led brightness", + icon="mdi:brightness-6", + short_name=ATTR_LED_BRIGHTNESS, + options=["Bright", "Dim", "Off"], + service=SERVICE_SET_LED_BRIGHTNESS, + ), +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Selectors from a config entry.""" + if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + return + + entities = [] + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + model = config_entry.data[CONF_MODEL] + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: + name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] + else: + name = config_entry.title + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + + if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: + entity_class = XiaomiAirHumidifierSelector + elif model in [MODEL_AIRHUMIDIFIER_CA4]: + entity_class = XiaomiAirHumidifierMiotSelector + elif model in MODELS_HUMIDIFIER: + entity_class = XiaomiAirHumidifierSelector + else: + return + + for selector in SELECTOR_TYPES.values(): + entities.append( + entity_class( + f"{name} {selector.name}", + device, + config_entry, + f"{selector.short_name}_{config_entry.unique_id}", + selector, + coordinator, + ) + ) + + async_add_entities(entities) + + +class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): + """Representation of a generic Xiaomi attribute selector.""" + + def __init__(self, name, device, entry, unique_id, selector, coordinator): + """Initialize the generic Xiaomi attribute selector.""" + super().__init__(name, device, entry, unique_id, coordinator) + self._attr_icon = selector.icon + self._controller = selector + self._attr_options = self._controller.options + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + +class XiaomiAirHumidifierSelector(XiaomiSelector): + """Representation of a Xiaomi Air Humidifier selector.""" + + def __init__(self, name, device, entry, unique_id, controller, coordinator): + """Initialize the plug switch.""" + super().__init__(name, device, entry, unique_id, controller, coordinator) + self._current_led_brightness = self._extract_value_from_attribute( + self.coordinator.data, self._controller.short_name + ) + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._current_led_brightness = self._extract_value_from_attribute( + self.coordinator.data, self._controller.short_name + ) + self.async_write_ha_state() + + @property + def current_option(self): + """Return the current option.""" + return self.led_brightness + + async def async_select_option(self, option: str) -> None: + """Set an option of the miio device.""" + if option not in self.options: + raise ValueError( + f"Selection '{option}' is not a valid {self._controller.name}" + ) + await self.async_set_led_brightness(option) + + @property + def led_brightness(self): + """Return the current led brightness.""" + return LED_BRIGHTNESS_REVERSE_MAP.get(self._current_led_brightness) + + async def async_set_led_brightness(self, brightness: str): + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirhumidifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self.async_write_ha_state() + + +class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Humidifier (MiOT protocol) selector.""" + + @property + def led_brightness(self): + """Return the current led brightness.""" + return LED_BRIGHTNESS_REVERSE_MAP_MIOT.get(self._current_led_brightness) + + async def async_set_led_brightness(self, brightness: str): + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirhumidifierMiotLedBrightness(LED_BRIGHTNESS_MAP_MIOT[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP_MIOT[brightness] + self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 5d271a772b9..3c28d8496e7 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,5 +1,6 @@ -"""Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +"""Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier.""" from dataclasses import dataclass +from enum import Enum import logging from miio import AirQualityMonitor, DeviceException @@ -20,6 +21,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, CONF_TOKEN, @@ -35,8 +37,18 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from .const import CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, KEY_COORDINATOR -from .device import XiaomiMiioEntity +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_GATEWAY, + CONF_MODEL, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, + KEY_MIGRATE_ENTITY_NAME, + MODELS_HUMIDIFIER_MIOT, +) +from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) @@ -59,42 +71,69 @@ ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" ATTR_SENSOR_STATE = "sensor_state" - -SUCCESS = ["ok"] +ATTR_WATER_LEVEL = "water_level" +ATTR_HUMIDITY = "humidity" +ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" @dataclass class SensorType: - """Class that holds device specific info for a xiaomi aqara sensor.""" + """Class that holds device specific info for a xiaomi aqara or humidifier sensor.""" unit: str = None icon: str = None device_class: str = None state_class: str = None + valid_min_value: float = None + valid_max_value: float = None -GATEWAY_SENSOR_TYPES = { +SENSOR_TYPES = { "temperature": SensorType( unit=TEMP_CELSIUS, - icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), "humidity": SensorType( unit=PERCENTAGE, - icon=None, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), "pressure": SensorType( unit=PRESSURE_HPA, - icon=None, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), "load_power": SensorType( - unit=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER + unit=POWER_WATT, + device_class=DEVICE_CLASS_POWER, ), + "water_level": SensorType( + unit=PERCENTAGE, + icon="mdi:water-check", + state_class=STATE_CLASS_MEASUREMENT, + valid_min_value=0.0, + valid_max_value=100.0, + ), + "actual_speed": SensorType( + unit="rpm", + icon="mdi:fast-forward", + state_class=STATE_CLASS_MEASUREMENT, + valid_min_value=200.0, + valid_max_value=2000.0, + ), +} + +HUMIDIFIER_SENSORS = { + ATTR_HUMIDITY: "humidity", + ATTR_TEMPERATURE: "temperature", +} + +HUMIDIFIER_SENSORS_MIOT = { + ATTR_HUMIDITY: "humidity", + ATTR_TEMPERATURE: "temperature", + ATTR_WATER_LEVEL: "water_level", + ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", } @@ -135,7 +174,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sub_devices = gateway.devices coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for sub_device in sub_devices.values(): - sensor_variables = set(sub_device.status) & set(GATEWAY_SENSOR_TYPES) + sensor_variables = set(sub_device.status) & set(SENSOR_TYPES) if sensor_variables: entities.extend( [ @@ -145,19 +184,90 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for variable in sensor_variables ] ) - - if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] - name = config_entry.title - unique_id = config_entry.unique_id + model = config_entry.data[CONF_MODEL] + device = None + sensors = [] + if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: + name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] + else: + name = config_entry.title - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + if model in MODELS_HUMIDIFIER_MIOT: + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + sensors = HUMIDIFIER_SENSORS_MIOT + elif model.startswith("zhimi.humidifier."): + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + sensors = HUMIDIFIER_SENSORS + else: + unique_id = config_entry.unique_id + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) - device = AirQualityMonitor(host, token) - entities.append(XiaomiAirQualityMonitor(name, device, config_entry, unique_id)) + device = AirQualityMonitor(host, token) + entities.append( + XiaomiAirQualityMonitor(name, device, config_entry, unique_id) + ) + for sensor in sensors: + entities.append( + XiaomiGenericSensor( + f"{name} {sensor.replace('_', ' ').title()}", + device, + config_entry, + f"{sensor}_{config_entry.unique_id}", + sensor, + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + ) + ) - async_add_entities(entities, update_before_add=True) + async_add_entities(entities) + + +class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): + """Representation of a Xiaomi Humidifier sensor.""" + + def __init__(self, name, device, entry, unique_id, attribute, coordinator): + """Initialize the entity.""" + super().__init__(name, device, entry, unique_id, coordinator) + + self._sensor_config = SENSOR_TYPES[attribute] + self._attr_device_class = self._sensor_config.device_class + self._attr_state_class = self._sensor_config.state_class + self._attr_icon = self._sensor_config.icon + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_unit_of_measurement = self._sensor_config.unit + self._device = device + self._entry = entry + self._attribute = attribute + self._state = None + + @property + def state(self): + """Return the state of the device.""" + self._state = self._extract_value_from_attribute( + self.coordinator.data, self._attribute + ) + if ( + self._sensor_config.valid_min_value + and self._state < self._sensor_config.valid_min_value + ) or ( + self._sensor_config.valid_max_value + and self._state > self._sensor_config.valid_max_value + ): + return None + return self._state + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): @@ -189,7 +299,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): @property def icon(self): - """Return the icon to use for device if any.""" + """Return the icon to use in the frontend.""" return self._icon @property @@ -247,22 +357,22 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): @property def icon(self): """Return the icon to use in the frontend.""" - return GATEWAY_SENSOR_TYPES[self._data_key].icon + return SENSOR_TYPES[self._data_key].icon @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return GATEWAY_SENSOR_TYPES[self._data_key].unit + return SENSOR_TYPES[self._data_key].unit @property def device_class(self): """Return the device class of this entity.""" - return GATEWAY_SENSOR_TYPES[self._data_key].device_class + return SENSOR_TYPES[self._data_key].device_class @property def state_class(self): """Return the state class of this entity.""" - return GATEWAY_SENSOR_TYPES[self._data_key].state_class + return SENSOR_TYPES[self._data_key].state_class @property def state(self): diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 5802ae2a00d..35ac4c7da4f 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -1,5 +1,7 @@ """Support for Xiaomi Smart WiFi Socket and Smart Power Strip.""" import asyncio +from dataclasses import dataclass +from enum import Enum from functools import partial import logging @@ -21,6 +23,7 @@ from homeassistant.const import ( CONF_NAME, CONF_TOKEN, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .const import ( @@ -29,13 +32,31 @@ from .const import ( CONF_GATEWAY, CONF_MODEL, DOMAIN, + FEATURE_FLAGS_AIRHUMIDIFIER, + FEATURE_FLAGS_AIRHUMIDIFIER_CA4, + FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, + FEATURE_SET_BUZZER, + FEATURE_SET_CHILD_LOCK, + FEATURE_SET_CLEAN, + FEATURE_SET_DRY, KEY_COORDINATOR, + KEY_DEVICE, + KEY_MIGRATE_ENTITY_NAME, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, + MODEL_AIRHUMIDIFIER_CB1, + MODELS_HUMIDIFIER, + SERVICE_SET_BUZZER, + SERVICE_SET_CHILD_LOCK, + SERVICE_SET_CLEAN, + SERVICE_SET_DRY, SERVICE_SET_POWER_MODE, SERVICE_SET_POWER_PRICE, SERVICE_SET_WIFI_LED_OFF, SERVICE_SET_WIFI_LED_ON, + SUCCESS, ) -from .device import XiaomiMiioEntity +from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) @@ -83,8 +104,10 @@ ATTR_POWER_MODE = "power_mode" ATTR_WIFI_LED = "wifi_led" ATTR_POWER_PRICE = "power_price" ATTR_PRICE = "price" - -SUCCESS = ["ok"] +ATTR_BUZZER = "buzzer" +ATTR_CHILD_LOCK = "child_lock" +ATTR_DRY = "dry" +ATTR_CLEAN = "clean_mode" FEATURE_SET_POWER_MODE = 1 FEATURE_SET_WIFI_LED = 2 @@ -121,6 +144,62 @@ SERVICE_TO_METHOD = { "method": "async_set_power_price", "schema": SERVICE_SCHEMA_POWER_PRICE, }, + SERVICE_SET_BUZZER: { + "method_on": "async_set_buzzer_on", + "method_off": "async_set_buzzer_off", + }, + SERVICE_SET_CHILD_LOCK: { + "method_on": "async_set_child_lock_on", + "method_off": "async_set_child_lock_off", + }, + SERVICE_SET_DRY: { + "method_on": "async_set_dry_on", + "method_off": "async_set_dry_off", + }, + SERVICE_SET_CLEAN: { + "method_on": "async_set_clean_on", + "method_off": "async_set_clean_off", + }, +} + + +@dataclass +class SwitchType: + """Class that holds device specific info for a xiaomi aqara or humidifiers.""" + + name: str = None + short_name: str = None + icon: str = None + service: str = None + available_with_device_off: bool = True + + +SWITCH_TYPES = { + FEATURE_SET_BUZZER: SwitchType( + name="Buzzer", + icon="mdi:volume-high", + short_name=ATTR_BUZZER, + service=SERVICE_SET_BUZZER, + ), + FEATURE_SET_CHILD_LOCK: SwitchType( + name="Child Lock", + icon="mdi:lock", + short_name=ATTR_CHILD_LOCK, + service=SERVICE_SET_CHILD_LOCK, + ), + FEATURE_SET_DRY: SwitchType( + name="Dry Mode", + icon="mdi:hair-dryer", + short_name=ATTR_DRY, + service=SERVICE_SET_DRY, + ), + FEATURE_SET_CLEAN: SwitchType( + name="Clean Mode", + icon="mdi:sparkles", + short_name=ATTR_CLEAN, + service=SERVICE_SET_CLEAN, + available_with_device_off=False, + ), } @@ -140,14 +219,63 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the switch from a config entry.""" - entities = [] + if ( + config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY + or config_entry.data[CONF_MODEL] == "lumi.acpartner.v3" + ): + await async_setup_other_entry(hass, config_entry, async_add_entities) + else: + await async_setup_coordinated_entry(hass, config_entry, async_add_entities) + +async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): + """Set up the coordinated switch from a config entry.""" + entities = [] + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: + name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] + else: + name = config_entry.title + + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + device_features = 0 + + if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: + device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB + elif model in [MODEL_AIRHUMIDIFIER_CA4]: + device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4 + elif model in MODELS_HUMIDIFIER: + device_features = FEATURE_FLAGS_AIRHUMIDIFIER + + for feature, switch in SWITCH_TYPES.items(): + if feature & device_features: + entities.append( + XiaomiGenericCoordinatedSwitch( + f"{name} {switch.name}", + device, + config_entry, + f"{switch.short_name}_{unique_id}", + switch, + coordinator, + ) + ) + + async_add_entities(entities) + + +async def async_setup_other_entry(hass, config_entry, async_add_entities): + """Set up the other type switch from a config entry.""" + entities = [] host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] name = config_entry.title model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id - if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] # Gateway sub devices @@ -256,7 +384,131 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DOMAIN, plug_service, async_service_handler, schema=schema ) - async_add_entities(entities, update_before_add=True) + async_add_entities(entities) + + +class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): + """Representation of a Xiaomi Plug Generic.""" + + def __init__(self, name, device, entry, unique_id, switch, coordinator): + """Initialize the plug switch.""" + super().__init__(name, device, entry, unique_id, coordinator) + + self._attr_icon = switch.icon + self._controller = switch + self._attr_is_on = self._extract_value_from_attribute( + self.coordinator.data, self._controller.short_name + ) + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + # On state change the device doesn't provide the new state immediately. + self._attr_is_on = self._extract_value_from_attribute( + self.coordinator.data, self._controller.short_name + ) + self.async_write_ha_state() + + @property + def available(self): + """Return true when state is known.""" + if ( + super().available + and not self.coordinator.data.is_on + and not self._controller.available_with_device_off + ): + return False + return super().available + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + async def async_turn_on(self, **kwargs) -> None: + """Turn on an option of the miio device.""" + method = getattr(self, SERVICE_TO_METHOD[self._controller.service]["method_on"]) + if await method(): + # Write state back to avoid switch flips with a slow response + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn off an option of the miio device.""" + method = getattr( + self, SERVICE_TO_METHOD[self._controller.service]["method_off"] + ) + if await method(): + # Write state back to avoid switch flips with a slow response + self._attr_is_on = False + self.async_write_ha_state() + + async def async_set_buzzer_on(self) -> bool: + """Turn the buzzer on.""" + return await self._try_command( + "Turning the buzzer of the miio device on failed.", + self._device.set_buzzer, + True, + ) + + async def async_set_buzzer_off(self) -> bool: + """Turn the buzzer off.""" + return await self._try_command( + "Turning the buzzer of the miio device off failed.", + self._device.set_buzzer, + False, + ) + + async def async_set_child_lock_on(self) -> bool: + """Turn the child lock on.""" + return await self._try_command( + "Turning the child lock of the miio device on failed.", + self._device.set_child_lock, + True, + ) + + async def async_set_child_lock_off(self) -> bool: + """Turn the child lock off.""" + return await self._try_command( + "Turning the child lock of the miio device off failed.", + self._device.set_child_lock, + False, + ) + + async def async_set_dry_on(self) -> bool: + """Turn the dry mode on.""" + return await self._try_command( + "Turning the dry mode of the miio device on failed.", + self._device.set_dry, + True, + ) + + async def async_set_dry_off(self) -> bool: + """Turn the dry mode off.""" + return await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, + False, + ) + + async def async_set_clean_on(self) -> bool: + """Turn the dry mode on.""" + return await self._try_command( + "Turning the clean mode of the miio device on failed.", + self._device.set_clean_mode, + True, + ) + + async def async_set_clean_off(self) -> bool: + """Turn the dry mode off.""" + return await self._try_command( + "Turning the clean mode of the miio device off failed.", + self._device.set_clean_mode, + False, + ) class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): From 6d13466f8a9b157609227046e5ee542d1a261d0f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 28 Jul 2021 11:23:57 +0200 Subject: [PATCH 689/818] Remove unnecessary `init_integration()` call in NAM tests (#53609) --- tests/components/nam/test_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 506a81f7619..c5850ce719d 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -441,8 +441,6 @@ async def test_unique_id_migration(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature" - await init_integration(hass) - entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity" From 553521a76b51e4eae305ca531a1df90d4fccb37d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 11:50:13 +0200 Subject: [PATCH 690/818] Add mixin classes for required keys in EntityDescription (#53610) --- homeassistant/components/climacell/const.py | 3 - homeassistant/components/climacell/sensor.py | 2 +- homeassistant/components/melcloud/sensor.py | 52 ++++++++-------- homeassistant/components/netatmo/sensor.py | 63 ++++++++++---------- homeassistant/util/__init__.py | 4 +- tests/util/test_init.py | 1 + 6 files changed, 60 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index bcbd139028a..9e80c769abf 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -170,9 +170,6 @@ class ClimaCellSensorEntityDescription(SensorEntityDescription): "`unit_imperial` and `unit_metric` both need to be None or both need " "to be defined." ) - if self.name is None: # pragma: no cover - raise TypeError - self.name_ = self.name CC_SENSOR_TYPES = ( diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index d67cde18e0b..3f96dd9e02c 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -65,7 +65,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): self._attr_entity_registry_enabled_default = False self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" self._attr_unique_id = ( - f"{self._config_entry.unique_id}_{slugify(description.name_)}" + f"{self._config_entry.unique_id}_{slugify(description.name)}" ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} self._attr_unit_of_measurement = ( diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 104f463306f..6c303e8e3c3 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -22,21 +22,19 @@ from .const import DOMAIN @dataclass -class MelcloudSensorEntityDescription(SensorEntityDescription): +class MelcloudRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Any], float] + enabled: Callable[[Any], bool] + + +@dataclass +class MelcloudSensorEntityDescription( + SensorEntityDescription, MelcloudRequiredKeysMixin +): """Describes Melcloud sensor entity.""" - _value_fn: Callable[[Any], float] | None = None - _enabled: Callable[[Any], bool] | None = None - - def __post_init__(self) -> None: - """Ensure all required fields are set.""" - if self._value_fn is None: # pragma: no cover - raise TypeError - if self._enabled is None: # pragma: no cover - raise TypeError - self.value_fn = self._value_fn - self.enabled = self._enabled - ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( @@ -45,8 +43,8 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:thermometer", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - _value_fn=lambda x: x.device.room_temperature, - _enabled=lambda x: True, + value_fn=lambda x: x.device.room_temperature, + enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="energy", @@ -54,8 +52,8 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:factory", unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - _value_fn=lambda x: x.device.total_energy_consumed, - _enabled=lambda x: x.device.has_energy_consumed_meter, + value_fn=lambda x: x.device.total_energy_consumed, + enabled=lambda x: x.device.has_energy_consumed_meter, ), ) ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( @@ -65,8 +63,8 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:thermometer", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - _value_fn=lambda x: x.device.outside_temperature, - _enabled=lambda x: True, + value_fn=lambda x: x.device.outside_temperature, + enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="tank_temperature", @@ -74,8 +72,8 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:thermometer", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - _value_fn=lambda x: x.device.tank_temperature, - _enabled=lambda x: True, + value_fn=lambda x: x.device.tank_temperature, + enabled=lambda x: True, ), ) ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( @@ -85,8 +83,8 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:thermometer", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - _value_fn=lambda zone: zone.room_temperature, - _enabled=lambda x: True, + value_fn=lambda zone: zone.room_temperature, + enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="flow_temperature", @@ -94,8 +92,8 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:thermometer", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - _value_fn=lambda zone: zone.flow_temperature, - _enabled=lambda x: True, + value_fn=lambda zone: zone.flow_temperature, + enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="return_temperature", @@ -103,8 +101,8 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( icon="mdi:thermometer", unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - _value_fn=lambda zone: zone.return_temperature, - _enabled=lambda x: True, + value_fn=lambda zone: zone.return_temperature, + enabled=lambda x: True, ), ) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 58d4532e40d..14128aefa6a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -62,23 +62,22 @@ SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( @dataclass -class NetatmoSensorEntityDescription(SensorEntityDescription): +class NetatmoRequiredKeysMixin: + """Mixin for required keys.""" + + netatmo_name: str + + +@dataclass +class NetatmoSensorEntityDescription(SensorEntityDescription, NetatmoRequiredKeysMixin): """Describes Netatmo sensor entity.""" - _netatmo_name: str | None = None - - def __post_init__(self) -> None: - """Ensure all required attributes are set.""" - if self._netatmo_name is None: # pragma: no cover - raise TypeError - self.netatmo_name = self._netatmo_name - SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temperature", name="Temperature", - _netatmo_name="Temperature", + netatmo_name="Temperature", entity_registry_enabled_default=True, unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, @@ -86,14 +85,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temp_trend", name="Temperature trend", - _netatmo_name="temp_trend", + netatmo_name="temp_trend", entity_registry_enabled_default=False, icon="mdi:trending-up", ), NetatmoSensorEntityDescription( key="co2", name="CO2", - _netatmo_name="CO2", + netatmo_name="CO2", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, device_class=DEVICE_CLASS_CO2, @@ -101,7 +100,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="pressure", name="Pressure", - _netatmo_name="Pressure", + netatmo_name="Pressure", entity_registry_enabled_default=True, unit_of_measurement=PRESSURE_MBAR, device_class=DEVICE_CLASS_PRESSURE, @@ -109,14 +108,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="pressure_trend", name="Pressure trend", - _netatmo_name="pressure_trend", + netatmo_name="pressure_trend", entity_registry_enabled_default=False, icon="mdi:trending-up", ), NetatmoSensorEntityDescription( key="noise", name="Noise", - _netatmo_name="Noise", + netatmo_name="Noise", entity_registry_enabled_default=True, unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", @@ -124,7 +123,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="humidity", name="Humidity", - _netatmo_name="Humidity", + netatmo_name="Humidity", entity_registry_enabled_default=True, unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, @@ -132,7 +131,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="rain", name="Rain", - _netatmo_name="Rain", + netatmo_name="Rain", entity_registry_enabled_default=True, unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", @@ -140,7 +139,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="sum_rain_1", name="Rain last hour", - _netatmo_name="sum_rain_1", + netatmo_name="sum_rain_1", entity_registry_enabled_default=False, unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", @@ -148,7 +147,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="sum_rain_24", name="Rain today", - _netatmo_name="sum_rain_24", + netatmo_name="sum_rain_24", entity_registry_enabled_default=True, unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", @@ -156,7 +155,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="battery_percent", name="Battery Percent", - _netatmo_name="battery_percent", + netatmo_name="battery_percent", entity_registry_enabled_default=True, unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, @@ -164,14 +163,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windangle", name="Direction", - _netatmo_name="WindAngle", + netatmo_name="WindAngle", entity_registry_enabled_default=True, icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="windangle_value", name="Angle", - _netatmo_name="WindAngle", + netatmo_name="WindAngle", entity_registry_enabled_default=False, unit_of_measurement=DEGREE, icon="mdi:compass-outline", @@ -179,7 +178,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windstrength", name="Wind Strength", - _netatmo_name="WindStrength", + netatmo_name="WindStrength", entity_registry_enabled_default=True, unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", @@ -187,14 +186,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="gustangle", name="Gust Direction", - _netatmo_name="GustAngle", + netatmo_name="GustAngle", entity_registry_enabled_default=False, icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="gustangle_value", name="Gust Angle", - _netatmo_name="GustAngle", + netatmo_name="GustAngle", entity_registry_enabled_default=False, unit_of_measurement=DEGREE, icon="mdi:compass-outline", @@ -202,7 +201,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="guststrength", name="Gust Strength", - _netatmo_name="GustStrength", + netatmo_name="GustStrength", entity_registry_enabled_default=False, unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", @@ -210,21 +209,21 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="reachable", name="Reachability", - _netatmo_name="reachable", + netatmo_name="reachable", entity_registry_enabled_default=False, icon="mdi:signal", ), NetatmoSensorEntityDescription( key="rf_status", name="Radio", - _netatmo_name="rf_status", + netatmo_name="rf_status", entity_registry_enabled_default=False, icon="mdi:signal", ), NetatmoSensorEntityDescription( key="rf_status_lvl", name="Radio Level", - _netatmo_name="rf_status", + netatmo_name="rf_status", entity_registry_enabled_default=False, unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, @@ -232,14 +231,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="wifi_status", name="Wifi", - _netatmo_name="wifi_status", + netatmo_name="wifi_status", entity_registry_enabled_default=False, icon="mdi:wifi", ), NetatmoSensorEntityDescription( key="wifi_status_lvl", name="Wifi Level", - _netatmo_name="wifi_status", + netatmo_name="wifi_status", entity_registry_enabled_default=False, unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, @@ -247,7 +246,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="health_idx", name="Health", - _netatmo_name="health_idx", + netatmo_name="health_idx", entity_registry_enabled_default=True, icon="mdi:cloud", ), diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index bf11103b3fa..60f3e409f06 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -79,9 +79,9 @@ def sanitize_path(path: str) -> str: return path -def slugify(text: str, *, separator: str = "_") -> str: +def slugify(text: str | None, *, separator: str = "_") -> str: """Slugify a given text.""" - if text == "": + if text == "" or text is None: return "" slug = unicode_slug.slugify(text, separator=separator) return "unknown" if slug == "" else slug diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 34e95013b26..7a4f13cb767 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -74,6 +74,7 @@ def test_slugify(): assert util.slugify("$$$") == "unknown" assert util.slugify("$something") == "something" assert util.slugify("") == "" + assert util.slugify(None) == "" def test_repr_helper(): From 6299f58bd788aa67f2c44a5d1568de8dffb1ceec Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 28 Jul 2021 11:52:55 +0200 Subject: [PATCH 691/818] Remove Rituals DiffuserSwitch extra_state_attributes (#53611) --- homeassistant/components/rituals_perfume_genie/switch.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 924a38dfde8..180c144a358 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -43,14 +43,6 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity): super().__init__(diffuser, coordinator, "") self._attr_is_on = self._diffuser.is_on - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device state attributes.""" - return { - "fan_speed": self._diffuser.perfume_amount, - "room_size": self._diffuser.room_size, - } - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._diffuser.turn_on() From c81371f82bf8355a5196ce1da76df9d35c9cbae0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 28 Jul 2021 12:03:24 +0200 Subject: [PATCH 692/818] Clean device initialization log for Xiaomi Miio humidifiers (#53612) --- homeassistant/components/xiaomi_miio/__init__.py | 3 +++ homeassistant/components/xiaomi_miio/humidifier.py | 6 +----- homeassistant/components/xiaomi_miio/number.py | 7 ------- homeassistant/components/xiaomi_miio/select.py | 8 -------- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index afc6783a0bb..4ca64fc175c 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -104,6 +104,9 @@ async def async_create_miio_device_and_coordinator( if model not in MODELS_HUMIDIFIER: return + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + if model in MODELS_HUMIDIFIER_MIOT: device = AirHumidifierMiot(host, token) else: diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 1535a8772ab..9d9f49229d1 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -13,7 +13,7 @@ from homeassistant.components.humidifier.const import ( DEVICE_CLASS_HUMIDIFIER, SUPPORT_MODES, ) -from homeassistant.const import ATTR_MODE, CONF_HOST, CONF_TOKEN +from homeassistant.const import ATTR_MODE from homeassistant.core import callback from homeassistant.util.percentage import percentage_to_ranged_value @@ -49,8 +49,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return entities = [] - host = config_entry.data[CONF_HOST] - token = config_entry.data[CONF_TOKEN] model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -59,8 +57,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): else: name = config_entry.title - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) - if model in MODELS_HUMIDIFIER_MIOT: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMiot( diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index f81f46967ab..4d0e92b104f 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -1,10 +1,8 @@ """Motor speed support for Xiaomi Mi Air Humidifier.""" from dataclasses import dataclass from enum import Enum -import logging from homeassistant.components.number import NumberEntity -from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import callback from .const import ( @@ -20,8 +18,6 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity -_LOGGER = logging.getLogger(__name__) - ATTR_MOTOR_SPEED = "motor_speed" @@ -59,8 +55,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return - host = config_entry.data[CONF_HOST] - token = config_entry.data[CONF_TOKEN] model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -69,7 +63,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): else: name = config_entry.title - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) if model not in [MODEL_AIRHUMIDIFIER_CA4]: return diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 40a236aab6a..055d8073739 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -1,13 +1,11 @@ """Support led_brightness for Mi Air Humidifier.""" from dataclasses import dataclass from enum import Enum -import logging from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness from homeassistant.components.select import SelectEntity -from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import callback from .const import ( @@ -27,8 +25,6 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity -_LOGGER = logging.getLogger(__name__) - ATTR_LED_BRIGHTNESS = "led_brightness" @@ -68,8 +64,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return entities = [] - host = config_entry.data[CONF_HOST] - token = config_entry.data[CONF_TOKEN] model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -78,8 +72,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): else: name = config_entry.title - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) - if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: entity_class = XiaomiAirHumidifierSelector elif model in [MODEL_AIRHUMIDIFIER_CA4]: From ec5d55dc30e6c887a15feb34362a313ac58ebb98 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 28 Jul 2021 23:56:45 +1200 Subject: [PATCH 693/818] Auto reset on value going back to 0 in ESPHome (#53592) * ESPHome - Auto reset on value going back to 0 * Remove logging lines * Remove useless stuff * Move callback to sensor class Wrap `track_change_event` in `async_on_remove` * Convert to using internal callbacks and RestoreEntity * Don't document fixmes? * Review fixes * Review fixes Co-authored-by: Otto winter --- homeassistant/components/esphome/__init__.py | 42 +++++------ homeassistant/components/esphome/camera.py | 33 +++------ homeassistant/components/esphome/sensor.py | 78 +++++++++++++++++++- 3 files changed, 105 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index e4976202983..2efe005230f 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -712,7 +712,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: _InfoT = TypeVar("_InfoT", bound=EntityInfo) -_EntityT = TypeVar("_EntityT", bound="EsphomeBaseEntity[Any,Any]") +_EntityT = TypeVar("_EntityT", bound="EsphomeEntity[Any,Any]") _StateT = TypeVar("_StateT", bound=EntityState) @@ -850,7 +850,7 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): return self._inverse[value] -class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]): +class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" def __init__( @@ -882,6 +882,22 @@ class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]): ) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ( + f"esphome_{self._entry_id}" + f"_update_{self._component_key}_{self._key}" + ), + self._on_state_update, + ) + ) + + @callback + def _on_state_update(self) -> None: + # Behavior can be changed in child classes + self.async_write_ha_state() + @callback def _on_device_update(self) -> None: """Update the entity state when device info has changed.""" @@ -890,7 +906,7 @@ class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]): # Only update the HA state when the full state arrives # through the next entity state packet. return - self.async_write_ha_state() + self._on_state_update() @property def _entry_id(self) -> str: @@ -962,23 +978,3 @@ class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]): def should_poll(self) -> bool: """Disable polling.""" return False - - -class EsphomeEntity(EsphomeBaseEntity[_InfoT, _StateT]): - """Define a generic esphome entity.""" - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - await super().async_added_to_hass() - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{self._entry_id}" - f"_update_{self._component_key}_{self._key}" - ), - self.async_write_ha_state, - ) - ) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index e8f37c3d191..938d78362f7 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -10,11 +10,10 @@ from aiohttp import web from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import EsphomeBaseEntity, platform_async_setup_entry +from . import EsphomeEntity, platform_async_setup_entry async def async_setup_entry( @@ -32,34 +31,22 @@ async def async_setup_entry( ) -class EsphomeCamera(Camera, EsphomeBaseEntity[CameraInfo, CameraState]): +class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): """A camera implementation for ESPHome.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize.""" Camera.__init__(self) - EsphomeBaseEntity.__init__(self, *args, **kwargs) + EsphomeEntity.__init__(self, *args, **kwargs) self._image_cond = asyncio.Condition() - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - await super().async_added_to_hass() - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{self._entry_id}" - f"_update_{self._component_key}_{self._key}" - ), - self._on_state_update, - ) - ) - - async def _on_state_update(self) -> None: + @callback + def _on_state_update(self) -> None: """Notify listeners of new image when update arrives.""" - self.async_write_ha_state() + super()._on_state_update() + self.hass.async_create_task(self._on_state_update_coro()) + + async def _on_state_update_coro(self) -> None: async with self._image_cond: self._image_cond.notify_all() diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 97cb5718903..6a2b51498f0 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -1,6 +1,8 @@ """Support for esphome sensors.""" from __future__ import annotations +from contextlib import suppress +from datetime import datetime import math from typing import cast @@ -11,6 +13,7 @@ from aioesphomeapi import ( TextSensorInfo, TextSensorState, ) +from aioesphomeapi.model import LastResetType import voluptuous as vol from homeassistant.components.sensor import ( @@ -20,9 +23,10 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt from . import ( @@ -71,9 +75,79 @@ _STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMap ) -class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): +class EsphomeSensor( + EsphomeEntity[SensorInfo, SensorState], SensorEntity, RestoreEntity +): """A sensor implementation for esphome.""" + _old_state: float | None = None + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + + if self._static_info.last_reset_type != LastResetType.AUTO: + return + + # Logic to restore old state for last_reset_type AUTO: + last_state = await self.async_get_last_state() + if last_state is None: + return + + if "last_reset" in last_state.attributes: + self._attr_last_reset = dt.as_utc( + datetime.fromisoformat(last_state.attributes["last_reset"]) + ) + + with suppress(ValueError): + self._old_state = float(last_state.state) + + @callback + def _on_state_update(self) -> None: + """Check last_reset when new state arrives.""" + if self._static_info.last_reset_type == LastResetType.NEVER: + self._attr_last_reset = dt.utc_from_timestamp(0) + + if self._static_info.last_reset_type != LastResetType.AUTO: + super()._on_state_update() + return + + # Last reset type AUTO logic for the last_reset property + # In this mode we automatically determine if an accumulator reset + # has taken place. + # We compare the last valid value (_old_state) with the new one. + # If the value has reset to 0 or has significantly reduced we say + # it has reset. + new_state: float | None = None + state = cast("str | None", self.state) + if state is not None: + with suppress(ValueError): + new_state = float(state) + + did_reset = False + if new_state is None: + # New state is not a float - we'll detect the reset once we get valid data again + did_reset = False + elif self._old_state is None: + # First measurement we ever got for this sensor, always a reset + did_reset = True + elif new_state == 0: + # don't set reset if both old and new are 0 + # we would already have detected the reset on the last state + did_reset = self._old_state != 0 + elif new_state < self._old_state: + did_reset = True + + # Set last_reset to now if we detected a reset + if did_reset: + self._attr_last_reset = dt.utcnow() + + if new_state is not None: + # Only write to old_state if the new one contains actual data + self._old_state = new_state + + super()._on_state_update() + @property def icon(self) -> str | None: """Return the icon.""" From 8d652b28e2790be695df154589d7a41cf30515fd Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 28 Jul 2021 14:19:21 +0200 Subject: [PATCH 694/818] Add Khadas VIM3 (#53616) * Add Khadas VIM3 Add Khadas VIM3 machine. VIM3 is based on Amlogic A311D SoC with 2xCortex-A53 and 4xCortex-A73. * Use latest builder which supports khadas-vim3 --- .github/workflows/builder.yml | 5 +++-- machine/khadas-vim3 | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 machine/khadas-vim3 diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 0c30f6887b2..89a408c3b6a 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -115,7 +115,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.06.2 + uses: home-assistant/builder@2021.07.0 with: args: | $BUILD_ARGS \ @@ -134,6 +134,7 @@ jobs: machine: - generic-x86-64 - intel-nuc + - khadas-vim3 - odroid-c2 - odroid-c4 - odroid-n2 @@ -167,7 +168,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.06.2 + uses: home-assistant/builder@2021.07.0 with: args: | $BUILD_ARGS \ diff --git a/machine/khadas-vim3 b/machine/khadas-vim3 new file mode 100644 index 00000000000..be07d6c8aba --- /dev/null +++ b/machine/khadas-vim3 @@ -0,0 +1,5 @@ +ARG BUILD_VERSION +FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION + +RUN apk --no-cache add \ + usbutils From babd9f048faa7eae01e6c03e64f49e07f265d8ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 10:49:43 -0500 Subject: [PATCH 695/818] Bump zeroconf to 0.33.2 (#53625) - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.33.1...0.33.2 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e41b30ea26c..ee1e9a8e1ab 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.33.1"], + "requirements": ["zeroconf==0.33.2"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80fc4359fc6..11d5b90bf50 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.1 +zeroconf==0.33.2 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index eb2781ac5e2..fddbf5e5c1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2438,7 +2438,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.1 +zeroconf==0.33.2 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce04a77f864..c2d2054619b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.1 +zeroconf==0.33.2 # homeassistant.components.zha zha-quirks==0.0.59 From 4df56bad9e16d144b7fdfbebb956163a4fdb2247 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 Jul 2021 17:49:55 +0200 Subject: [PATCH 696/818] Remove currency from energy, use core config (#53615) Co-authored-by: Paulus Schoutsen --- homeassistant/components/energy/data.py | 3 --- homeassistant/components/energy/sensor.py | 8 +++++--- homeassistant/components/energy/websocket_api.py | 1 - tests/components/energy/test_sensor.py | 14 +++++++------- tests/components/energy/test_websocket_api.py | 1 - 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 7e867e36bfc..c053dea4741 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -92,7 +92,6 @@ class DeviceConsumption(TypedDict): class EnergyPreferences(TypedDict): """Dictionary holding the energy data.""" - currency: str energy_sources: list[SourceType] device_consumption: list[DeviceConsumption] @@ -230,7 +229,6 @@ class EnergyManager: def default_preferences() -> EnergyPreferences: """Return default preferences.""" return { - "currency": "€", "energy_sources": [], "device_consumption": [], } @@ -243,7 +241,6 @@ class EnergyManager: data = self.data.copy() for key in ( - "currency", "energy_sources", "device_consumption", ): diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1a4dd4b7e41..773abbbe6b9 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -124,7 +124,6 @@ async def _process_manager_data( current_entities[key] = EnergyCostSensor( adapter, - manager.data["currency"], untyped_flow, ) to_add.append(current_entities[key]) @@ -142,7 +141,6 @@ class EnergyCostSensor(SensorEntity): def __init__( self, adapter: FlowAdapter, - currency: str, flow: dict, ) -> None: """Initialize the sensor.""" @@ -152,7 +150,6 @@ class EnergyCostSensor(SensorEntity): self.entity_id = f"{flow[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" self._attr_device_class = DEVICE_CLASS_MONETARY self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_unit_of_measurement = currency self._flow = flow self._last_energy_sensor_state: State | None = None @@ -256,3 +253,8 @@ class EnergyCostSensor(SensorEntity): def update_config(self, flow: dict) -> None: """Update the config.""" self._flow = flow + + @property + def unit_of_measurement(self) -> str | None: + """Return the units of measurement.""" + return self.hass.config.currency diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index 53e4aa7714c..d1c8869a1c2 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -82,7 +82,6 @@ def ws_get_prefs( @websocket_api.websocket_command( { vol.Required("type"): "energy/save_prefs", - vol.Optional("currency"): str, vol.Optional("energy_sources"): ENERGY_SOURCE_SCHEMA, vol.Optional("device_consumption"): [DEVICE_CONSUMPTION_SCHEMA], } diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index b6e0fc77188..f3af93c06c1 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -146,7 +146,7 @@ async def test_cost_sensor_price_entity( if initial_cost != "unknown": assert state.attributes[ATTR_LAST_RESET] == now.isoformat() assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "€" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities if initial_energy is None: @@ -161,7 +161,7 @@ async def test_cost_sensor_price_entity( assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY assert state.attributes[ATTR_LAST_RESET] == now.isoformat() assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "€" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled # # entity_registry = er.async_get(hass) @@ -172,7 +172,7 @@ async def test_cost_sensor_price_entity( hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) - assert state.state == "10.0" # 0 € + (10-0) kWh * 1 €/kWh = 10 € + assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR # Nothing happens when price changes if price_entity is not None: @@ -186,13 +186,13 @@ async def test_cost_sensor_price_entity( msg = await client.receive_json() assert msg["success"] state = hass.states.get(cost_sensor_entity_id) - assert state.state == "10.0" # 10 € + (10-10) kWh * 2 €/kWh = 10 € + assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR # Additional consumption is using the new price hass.states.async_set(usage_sensor_entity_id, "14.5", {"last_reset": last_reset}) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) - assert state.state == "19.0" # 10 € + (14.5-10) kWh * 2 €/kWh = 19 € + assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR # Check generated statistics await async_wait_recording_done_without_instance(hass) @@ -205,13 +205,13 @@ async def test_cost_sensor_price_entity( hass.states.async_set(usage_sensor_entity_id, "4", {"last_reset": last_reset}) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) - assert state.state == "0.0" # 0 € + (4-4) kWh * 2 €/kWh = 0 € + assert state.state == "0.0" # 0 EUR + (4-4) kWh * 2 EUR/kWh = 0 EUR # Energy use bumped to 10 kWh hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) - assert state.state == "12.0" # 0 € + (10-4) kWh * 2 €/kWh = 12 € + assert state.state == "12.0" # 0 EUR + (10-4) kWh * 2 EUR/kWh = 12 EUR # Check generated statistics await async_wait_recording_done_without_instance(hass) diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index a37850dd566..80ede3a7548 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -59,7 +59,6 @@ async def test_save_preferences(hass, hass_ws_client, hass_storage) -> None: assert msg["result"] == default_prefs new_prefs = { - "currency": "$", "energy_sources": [ { "type": "grid", From 419bcd939b9fd3807c9606a640685e7c590afac3 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 28 Jul 2021 13:53:34 -0300 Subject: [PATCH 697/818] Fix broadlink creating duplicate unique IDs (2) (#53622) --- homeassistant/components/broadlink/switch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 0f380b6cca2..9fb7215e2a9 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -146,7 +146,6 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): self._attr_assumed_state = True self._attr_device_class = DEVICE_CLASS_SWITCH self._attr_name = f"{device.name} Switch" - self._attr_unique_id = device.unique_id async def async_added_to_hass(self): """Call when the switch is added to hass.""" From 52df6a6558ce01b8da6c7b7f40cb64ab2f956f85 Mon Sep 17 00:00:00 2001 From: Frederic Seiler Date: Wed, 28 Jul 2021 18:54:19 +0200 Subject: [PATCH 698/818] Add deCONZ support for Legrand Self-e ZGP switches (#53008) * Add deCONZ support for Legrand Self-e ZGP switches Legrand Self-e ZLGP17 (0 677 23L) Legrand Self-e ZLGP18 (0 677 24L) * Add the 4 scenes switch (ZLGP15) and update the model name of the toggle switch * Update events --- .../components/deconz/device_trigger.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 40a49e111d8..5beaba2c5a5 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -450,6 +450,20 @@ GIRA_JUNG_SWITCH = { (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002}, } +LEGRAND_ZGP_TOGGLE_SWITCH_MODEL = "LEGRANDZGPTOGGLESWITCH" +LEGRAND_ZGP_TOGGLE_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, +} + +LEGRAND_ZGP_SCENE_SWITCH_MODEL = "LEGRANDZGPSCENESWITCH" +LEGRAND_ZGP_SCENE_SWITCH = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2002}, + (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4002}, +} + LIDL_SILVERCREST_DOORBELL_MODEL = "HG06668" LIDL_SILVERCREST_DOORBELL = { (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002}, @@ -569,6 +583,8 @@ REMOTES = { GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH, GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH, JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH, + LEGRAND_ZGP_TOGGLE_SWITCH_MODEL: LEGRAND_ZGP_TOGGLE_SWITCH, + LEGRAND_ZGP_SCENE_SWITCH_MODEL: LEGRAND_ZGP_SCENE_SWITCH, LIDL_SILVERCREST_DOORBELL_MODEL: LIDL_SILVERCREST_DOORBELL, LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, From deb47517ca5e9f0284414788791d4b86dd88af25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 28 Jul 2021 18:58:15 +0200 Subject: [PATCH 699/818] Upgrade ns-api to 3.0.5 (#53620) --- homeassistant/components/nederlandse_spoorwegen/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 92de680c17a..a94bf08f7c3 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -2,7 +2,7 @@ "domain": "nederlandse_spoorwegen", "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", - "requirements": ["nsapi==3.0.4"], + "requirements": ["nsapi==3.0.5"], "codeowners": ["@YarmoM"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index fddbf5e5c1d..0dd52d592ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1046,7 +1046,7 @@ notifications-android-tv==0.1.2 notify-events==1.0.4 # homeassistant.components.nederlandse_spoorwegen -nsapi==3.0.4 +nsapi==3.0.5 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.1.0 From 03308b62c19420fff1155317fcc1df1123a073e9 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 28 Jul 2021 18:58:45 +0200 Subject: [PATCH 700/818] Remove CONNECTION_CLASS from Yale Smart Alarm ConfigFlow (#53629) --- homeassistant/components/yale_smart_alarm/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index 828d308b0a0..7538c6e40ca 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -31,7 +31,6 @@ class YaleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Yale integration.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL entry: config_entries.ConfigEntry From 4e4bb2e5a89431a67285b98b99076e07a5836578 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 28 Jul 2021 19:04:11 +0200 Subject: [PATCH 701/818] Test KNX events (#53433) * test knx_event * use async_capture_events --- tests/components/knx/conftest.py | 4 +- tests/components/knx/test_events.py | 83 +++++++++++++++++++++++++++ tests/components/knx/test_services.py | 16 +++--- 3 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 tests/components/knx/test_events.py diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 26f8f1eabac..0dc8749830e 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -19,6 +19,8 @@ from homeassistant.setup import async_setup_component class KNXTestKit: """Test helper for the KNX integration.""" + INDIVIDUAL_ADDRESS = "1.2.3" + def __init__(self, hass: HomeAssistant): """Init KNX test helper class.""" self.hass: HomeAssistant = hass @@ -148,7 +150,7 @@ class KNXTestKit: destination_address=GroupAddress(group_address), direction=TelegramDirection.INCOMING, payload=payload, - source_address=IndividualAddress("1.2.3"), + source_address=IndividualAddress(self.INDIVIDUAL_ADDRESS), ) ) await self.hass.async_block_till_done() diff --git a/tests/components/knx/test_events.py b/tests/components/knx/test_events.py new file mode 100644 index 00000000000..6a9e021ff53 --- /dev/null +++ b/tests/components/knx/test_events.py @@ -0,0 +1,83 @@ +"""Test KNX events.""" + +from homeassistant.components.knx import CONF_KNX_EVENT_FILTER +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + +from tests.common import async_capture_events + + +async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): + """Test `knx_event` event.""" + test_group_a = "0/4/*" + test_address_a_1 = "0/4/0" + test_address_a_2 = "0/4/100" + test_group_b = "1/3-6/*" + test_address_b_1 = "1/3/0" + test_address_b_2 = "1/6/200" + test_group_c = "2/6/4,5" + test_address_c_1 = "2/6/4" + test_address_c_2 = "2/6/5" + test_address_d = "5/4/3" + events = async_capture_events(hass, "knx_event") + + async def test_event_data(address, payload): + await hass.async_block_till_done() + assert len(events) == 1 + event = events.pop() + assert event.data["data"] == payload + assert event.data["direction"] == "Incoming" + assert event.data["destination"] == address + if payload is None: + assert event.data["telegramtype"] == "GroupValueRead" + else: + assert event.data["telegramtype"] in ( + "GroupValueWrite", + "GroupValueResponse", + ) + assert event.data["source"] == KNXTestKit.INDIVIDUAL_ADDRESS + + await knx.setup_integration( + { + CONF_KNX_EVENT_FILTER: [ + test_group_a, + test_group_b, + test_group_c, + test_address_d, + ] + } + ) + + # no event received + await hass.async_block_till_done() + assert len(events) == 0 + + # receive telegrams for group addresses matching the filter + await knx.receive_write(test_address_a_1, True) + await test_event_data(test_address_a_1, True) + + await knx.receive_response(test_address_a_2, False) + await test_event_data(test_address_a_2, False) + + await knx.receive_write(test_address_b_1, (1,)) + await test_event_data(test_address_b_1, (1,)) + + await knx.receive_response(test_address_b_2, (255,)) + await test_event_data(test_address_b_2, (255,)) + + await knx.receive_write(test_address_c_1, (89, 43, 34, 11)) + await test_event_data(test_address_c_1, (89, 43, 34, 11)) + + await knx.receive_response(test_address_c_2, (255, 255, 255, 255)) + await test_event_data(test_address_c_2, (255, 255, 255, 255)) + + await knx.receive_read(test_address_d) + await test_event_data(test_address_d, None) + + # receive telegrams for group addresses not matching the filter + await knx.receive_write("0/5/0", True) + await knx.receive_write("1/7/0", True) + await knx.receive_write("2/6/6", True) + await hass.async_block_till_done() + assert len(events) == 0 diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index fe13a289a78..80ed51e6aec 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -4,6 +4,8 @@ from homeassistant.core import HomeAssistant from .conftest import KNXTestKit +from tests.common import async_capture_events + async def test_send(hass: HomeAssistant, knx: KNXTestKit): """Test `knx.send` service.""" @@ -74,17 +76,14 @@ async def test_read(hass: HomeAssistant, knx: KNXTestKit): async def test_event_register(hass: HomeAssistant, knx: KNXTestKit): """Test `knx.event_register` service.""" - events = [] + events = async_capture_events(hass, "knx_event") test_address = "1/2/3" - def listener(event): - events.append(event) - await knx.setup_integration({}) - hass.bus.async_listen("knx_event", listener) # no event registered await knx.receive_write(test_address, True) + await hass.async_block_till_done() assert len(events) == 0 # register event @@ -93,10 +92,10 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit): ) await knx.receive_write(test_address, True) await knx.receive_write(test_address, False) + await hass.async_block_till_done() assert len(events) == 2 - # remove event registration - events = [] + # remove event registration - no event added await hass.services.async_call( "knx", "event_register", @@ -104,7 +103,8 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.receive_write(test_address, True) - assert len(events) == 0 + await hass.async_block_till_done() + assert len(events) == 2 async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit): From 1b962887994c85586e106b04ef3e6dd7876fb98c Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 28 Jul 2021 19:09:48 +0200 Subject: [PATCH 702/818] Fix Yale Smart Alarm strings (#53627) --- homeassistant/components/yale_smart_alarm/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index 14bb48f8176..4fb61f5a5f1 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -11,15 +11,15 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]" + "name": "[%key:common::config_flow::data::name%]", + "area_id": "Area ID" } }, "reauth_confirm": { "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "name": "[%key:common::config_flow::data::name%]", "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]" } } From 9111fb60d7d39cd103013467cb887c5786e292a5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 28 Jul 2021 12:19:09 -0500 Subject: [PATCH 703/818] Include advertise_addr in Sonos logs when used (#53617) --- homeassistant/components/sonos/entity.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index b670ab96f2f..dadec82a939 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations import datetime import logging +import soco.config as soco_config from soco.core import SoCo from soco.exceptions import SoCoException @@ -65,10 +66,14 @@ class SonosEntity(Entity): async def async_poll(self, now: datetime.datetime) -> None: """Poll the entity if subscriptions fail.""" if not self.speaker.subscriptions_failed: + if soco_config.EVENT_ADVERTISE_IP: + listener_msg = f"{self.speaker.subscription_address} (advertising as {soco_config.EVENT_ADVERTISE_IP})" + else: + listener_msg = self.speaker.subscription_address _LOGGER.warning( - "%s cannot reach [%s], falling back to polling, functionality may be limited", + "%s cannot reach %s, falling back to polling, functionality may be limited", self.speaker.zone_name, - self.speaker.subscription_address, + listener_msg, ) self.speaker.subscriptions_failed = True await self.speaker.async_unsubscribe() From 3f7cc176a8a77130bec0073b623e5f31ba2ae34e Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Wed, 28 Jul 2021 19:27:31 +0200 Subject: [PATCH 704/818] Add climate support to Freedompro (#52720) * Update Freedompro * fix code and add test * add check for unsupported mode * add code for unsupported hvac_mode * HVAC_INVERT_MAP and fix test * change params hass to session * set const and add ValueError * fix ValueError text --- .../components/freedompro/__init__.py | 11 +- .../components/freedompro/climate.py | 138 ++++++++++++ tests/components/freedompro/test_climate.py | 203 ++++++++++++++++++ 3 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/freedompro/climate.py create mode 100644 tests/components/freedompro/test_climate.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 650e479d027..40d440d83eb 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,16 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "lock", "sensor", "switch"] +PLATFORMS = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "lock", + "sensor", + "switch", +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py new file mode 100644 index 00000000000..e37ae9dea1b --- /dev/null +++ b/homeassistant/components/freedompro/climate.py @@ -0,0 +1,138 @@ +"""Support for Freedompro climate.""" +import json +import logging + +from pyfreedompro import put_state + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +HVAC_MAP = { + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, +} + +HVAC_INVERT_MAP = {v: k for k, v in HVAC_MAP.items()} + +SUPPORTED_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL] + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro climate.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device( + aiohttp_client.async_get_clientsession(hass), api_key, device, coordinator + ) + for device in coordinator.data + if device["type"] == "thermostat" + ) + + +class Device(CoordinatorEntity, ClimateEntity): + """Representation of an Freedompro climate.""" + + _attr_hvac_modes = SUPPORTED_HVAC_MODES + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, session, api_key, device, coordinator): + """Initialize the Freedompro climate.""" + super().__init__(coordinator) + self._session = session + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": device["type"], + "manufacturer": "Freedompro", + } + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + self._attr_current_temperature = 0 + self._attr_target_temperature = 0 + self._attr_hvac_mode = HVAC_MODE_OFF + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self._attr_unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "currentTemperature" in state: + self._attr_current_temperature = state["currentTemperature"] + if "targetTemperature" in state: + self._attr_target_temperature = state["targetTemperature"] + if "heatingCoolingState" in state: + self._attr_hvac_mode = HVAC_MAP[state["heatingCoolingState"]] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_set_hvac_mode(self, hvac_mode): + """Async function to set mode to climate.""" + if hvac_mode not in SUPPORTED_HVAC_MODES: + raise ValueError(f"Got unsupported hvac_mode {hvac_mode}") + + payload = {} + payload["heatingCoolingState"] = HVAC_INVERT_MAP[hvac_mode] + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_set_temperature(self, **kwargs): + """Async function to set temperarture to climate.""" + payload = {} + if ATTR_HVAC_MODE in kwargs: + if kwargs[ATTR_HVAC_MODE] not in SUPPORTED_HVAC_MODES: + _LOGGER.error( + "Got unsupported hvac_mode %s, expected one of %s", + kwargs[ATTR_HVAC_MODE], + SUPPORTED_HVAC_MODES, + ) + return + payload["heatingCoolingState"] = HVAC_INVERT_MAP[kwargs[ATTR_HVAC_MODE]] + if ATTR_TEMPERATURE in kwargs: + payload["targetTemperature"] = kwargs[ATTR_TEMPERATURE] + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self.unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/freedompro/test_climate.py b/tests/components/freedompro/test_climate.py new file mode 100644 index 00000000000..36ec3309d24 --- /dev/null +++ b/tests/components/freedompro/test_climate.py @@ -0,0 +1,203 @@ +"""Tests for the Freedompro climate.""" + +from datetime import timedelta +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.climate.const import HVAC_MODE_AUTO +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + +uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI" + + +async def test_climate_get_state(hass, init_integration): + """Test states of the climate.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == "thermostat" + assert device.model == "thermostat" + + entity_id = "climate.thermostat" + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "thermostat" + + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + ] + + assert state.attributes[ATTR_MIN_TEMP] == 7 + assert state.attributes[ATTR_MAX_TEMP] == 35 + assert state.attributes[ATTR_TEMPERATURE] == 14 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 14 + + assert state.state == HVAC_MODE_HEAT + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + state_response["state"]["currentTemperature"] = 20 + state_response["state"]["targetTemperature"] = 21 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "thermostat" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.attributes[ATTR_TEMPERATURE] == 21 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20 + + +async def test_climate_set_off(hass, init_integration): + """Test set off climate.""" + init_integration + entity_registry = er.async_get(hass) + + entity_id = "climate.thermostat" + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "thermostat" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch( + "homeassistant.components.freedompro.climate.put_state" + ) as mock_put_state: + assert await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"heatingCoolingState": 0}') + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_HEAT + + +async def test_climate_set_unsupported_hvac_mode(hass, init_integration): + """Test set unsupported hvac mode climate.""" + init_integration + entity_registry = er.async_get(hass) + + entity_id = "climate.thermostat" + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "thermostat" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + blocking=True, + ) + + +async def test_climate_set_temperature(hass, init_integration): + """Test set temperature climate.""" + init_integration + entity_registry = er.async_get(hass) + + entity_id = "climate.thermostat" + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "thermostat" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + with patch( + "homeassistant.components.freedompro.climate.put_state" + ) as mock_put_state: + assert await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_HVAC_MODE: HVAC_MODE_OFF, + ATTR_TEMPERATURE: 25, + }, + blocking=True, + ) + mock_put_state.assert_called_once_with( + ANY, ANY, ANY, '{"heatingCoolingState": 0, "targetTemperature": 25.0}' + ) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TEMPERATURE] == 21 + + +async def test_climate_set_temperature_unsupported_hvac_mode(hass, init_integration): + """Test set temperature climate unsupported hvac mode.""" + init_integration + entity_registry = er.async_get(hass) + + entity_id = "climate.thermostat" + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == "thermostat" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_HVAC_MODE: HVAC_MODE_AUTO, + ATTR_TEMPERATURE: 25, + }, + blocking=True, + ) From 86b126c34a157bba01b7d85f68ac134f321cedb2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 28 Jul 2021 13:39:58 -0400 Subject: [PATCH 705/818] Use entity class attributes for cmus (#53458) --- homeassistant/components/cmus/media_player.py | 95 +++++-------------- 1 file changed, 24 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 3968ebbe9d7..651c584bc4c 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -98,6 +98,9 @@ class CmusRemote: class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" + _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_supported_features = SUPPORT_CMUS + def __init__(self, device, name, server): """Initialize the CMUS device.""" @@ -106,7 +109,7 @@ class CmusDevice(MediaPlayerEntity): auto_name = f"cmus-{server}" else: auto_name = "cmus-local" - self._name = name or auto_name + self._attr_name = name or auto_name self.status = {} def update(self): @@ -120,80 +123,30 @@ class CmusDevice(MediaPlayerEntity): self._remote.connect() else: self.status = status + if self.status.get("status") == "playing": + self._attr_state = STATE_PLAYING + elif self.status.get("status") == "paused": + self._attr_state = STATE_PAUSED + else: + self._attr_state = STATE_OFF + self._attr_media_content_id = self.status.get("file") + self._attr_media_duration = self.status.get("duration") + self._attr_media_title = self.status["tag"].get("title") + self._attr_media_artist = self.status["tag"].get("artist") + self._attr_media_track = self.status["tag"].get("tracknumber") + self._attr_media_album_name = self.status["tag"].get("album") + self._attr_media_album_artist = self.status["tag"].get("albumartist") + left = self.status["set"].get("vol_left")[0] + right = self.status["set"].get("vol_right")[0] + if left != right: + volume = float(left + right) / 2 + else: + volume = left + self._attr_volume_level = int(volume) / 100 return _LOGGER.warning("Received no status from cmus") - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the media state.""" - if self.status.get("status") == "playing": - return STATE_PLAYING - if self.status.get("status") == "paused": - return STATE_PAUSED - return STATE_OFF - - @property - def media_content_id(self): - """Content ID of current playing media.""" - return self.status.get("file") - - @property - def content_type(self): - """Content type of the current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self.status.get("duration") - - @property - def media_title(self): - """Title of current playing media.""" - return self.status["tag"].get("title") - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - return self.status["tag"].get("artist") - - @property - def media_track(self): - """Track number of current playing media, music track only.""" - return self.status["tag"].get("tracknumber") - - @property - def media_album_name(self): - """Album name of current playing media, music track only.""" - return self.status["tag"].get("album") - - @property - def media_album_artist(self): - """Album artist of current playing media, music track only.""" - return self.status["tag"].get("albumartist") - - @property - def volume_level(self): - """Return the volume level.""" - left = self.status["set"].get("vol_left")[0] - right = self.status["set"].get("vol_right")[0] - if left != right: - volume = float(left + right) / 2 - else: - volume = left - return int(volume) / 100 - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_CMUS - def turn_off(self): """Service to send the CMUS the command to stop playing.""" self._remote.cmus.player_stop() From 0e1eef57378004af57c5cf2286d6a4e32e7517cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 11:41:27 -0700 Subject: [PATCH 706/818] Bump frontend to 20210728.0 (#53634) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 470dae0b32e..cf835396fba 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210727.0" + "home-assistant-frontend==20210728.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 11d5b90bf50..0556c6e5452 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210727.0 +home-assistant-frontend==20210728.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 0dd52d592ac..0acdff35fb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -785,7 +785,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210727.0 +home-assistant-frontend==20210728.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2d2054619b..0b9513a7e54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -452,7 +452,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210727.0 +home-assistant-frontend==20210728.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c53e7bc8c2dd6f03359cd27b8fd2d42fc2e3b4e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 14:09:08 -0500 Subject: [PATCH 707/818] Only declare powerwall login failure after 5 attempts (#53635) --- homeassistant/components/powerwall/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 3bc4dc9b035..5560c51f72b 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -41,6 +41,8 @@ PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) +MAX_LOGIN_FAILURES = 5 + async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] @@ -111,17 +113,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from err await _migrate_old_unique_ids(hass, entry_id, powerwall_data) + login_failed_count = 0 async def async_update_data(): """Fetch data from API endpoint.""" # Check if we had an error before + nonlocal login_failed_count _LOGGER.debug("Checking if update failed") if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data _LOGGER.debug("Updating data") try: - return await _async_update_powerwall_data(hass, entry, power_wall) + data = await _async_update_powerwall_data(hass, entry, power_wall) except AccessDeniedError as err: if password is None: raise ConfigEntryAuthFailed from err @@ -131,7 +135,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(power_wall.login, "", password) return await _async_update_powerwall_data(hass, entry, power_wall) except AccessDeniedError as ex: - raise ConfigEntryAuthFailed from ex + login_failed_count += 1 + if login_failed_count == MAX_LOGIN_FAILURES: + raise ConfigEntryAuthFailed from ex + raise UpdateFailed( + f"Login attempt {login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry" + ) from ex + else: + login_failed_count = 0 + return data coordinator = DataUpdateCoordinator( hass, From 19245a5b63ce13817492615f89da3d2740f4d09f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 28 Jul 2021 21:09:45 +0200 Subject: [PATCH 708/818] Add CameraEntityDescription to camera integration (#53636) --- homeassistant/components/camera/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4791a963048..d1f354cc78e 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ import base64 import collections from collections.abc import Awaitable, Mapping from contextlib import suppress +from dataclasses import dataclass from datetime import datetime, timedelta import hashlib import logging @@ -46,7 +47,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity, entity_sources +from homeassistant.helpers.entity import Entity, EntityDescription, entity_sources from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType @@ -117,6 +118,11 @@ SCHEMA_WS_CAMERA_THUMBNAIL: Final = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.ex ) +@dataclass +class CameraEntityDescription(EntityDescription): + """A class that describes camera entities.""" + + @attr.s class Image: """Represent an image.""" From 67ac87e91595ea49747ee37cdb69d9e07fc8e355 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 28 Jul 2021 21:20:26 +0200 Subject: [PATCH 709/818] Fix missing supported_features when only custom presets for ESPHome (#53632) --- homeassistant/components/esphome/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 218f0fb319b..31d3e5f2320 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -228,9 +228,9 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti features |= SUPPORT_TARGET_TEMPERATURE if self.preset_modes: features |= SUPPORT_PRESET_MODE - if self._static_info.supported_fan_modes: + if self.fan_modes: features |= SUPPORT_FAN_MODE - if self._static_info.supported_swing_modes: + if self.swing_modes: features |= SUPPORT_SWING_MODE return features From 7bd46b7705d085808ab402673d48ce37c5ada5ba Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 28 Jul 2021 21:25:44 +0200 Subject: [PATCH 710/818] Tado, setup to return False and not ConfigEntryNotReady on RuntimeError (#53637) --- homeassistant/components/tado/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index db42df62154..3cb2abe90f6 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False except RuntimeError as exc: _LOGGER.error("Failed to setup tado: %s", exc) - return ConfigEntryNotReady + return False except requests.exceptions.Timeout as ex: raise ConfigEntryNotReady from ex except requests.exceptions.HTTPError as ex: From 743308ec92f1ed2c7a5cc56d6552c4adbe08cae9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Jul 2021 21:26:51 +0200 Subject: [PATCH 711/818] Bumped version to 2021.8.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0b8523bfa6f..69a06c04f02 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From a3a687f03741a284b7dfc257c3f2522801381a37 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 28 Jul 2021 21:41:11 +0200 Subject: [PATCH 712/818] Add renault integration (#39605) --- .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/renault/__init__.py | 45 +++ .../components/renault/config_flow.py | 92 +++++ homeassistant/components/renault/const.py | 15 + .../components/renault/manifest.json | 13 + .../components/renault/renault_coordinator.py | 72 ++++ .../components/renault/renault_entities.py | 103 ++++++ .../components/renault/renault_hub.py | 78 +++++ .../components/renault/renault_vehicle.py | 146 ++++++++ homeassistant/components/renault/sensor.py | 277 +++++++++++++++ homeassistant/components/renault/strings.json | 27 ++ .../components/renault/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/renault/__init__.py | 159 +++++++++ tests/components/renault/const.py | 328 ++++++++++++++++++ tests/components/renault/test_config_flow.py | 137 ++++++++ tests/components/renault/test_init.py | 85 +++++ tests/components/renault/test_sensor.py | 212 +++++++++++ .../renault/battery_status_charging.json | 18 + .../renault/battery_status_not_charging.json | 15 + .../fixtures/renault/charge_mode_always.json | 7 + .../renault/charge_mode_schedule.json | 7 + tests/fixtures/renault/cockpit_ev.json | 9 + tests/fixtures/renault/cockpit_fuel.json | 11 + tests/fixtures/renault/hvac_status.json | 7 + .../fixtures/renault/vehicle_captur_fuel.json | 108 ++++++ .../fixtures/renault/vehicle_captur_phev.json | 110 ++++++ tests/fixtures/renault/vehicle_zoe_40.json | 189 ++++++++++ tests/fixtures/renault/vehicle_zoe_50.json | 161 +++++++++ 33 files changed, 2478 insertions(+) create mode 100644 homeassistant/components/renault/__init__.py create mode 100644 homeassistant/components/renault/config_flow.py create mode 100644 homeassistant/components/renault/const.py create mode 100644 homeassistant/components/renault/manifest.json create mode 100644 homeassistant/components/renault/renault_coordinator.py create mode 100644 homeassistant/components/renault/renault_entities.py create mode 100644 homeassistant/components/renault/renault_hub.py create mode 100644 homeassistant/components/renault/renault_vehicle.py create mode 100644 homeassistant/components/renault/sensor.py create mode 100644 homeassistant/components/renault/strings.json create mode 100644 homeassistant/components/renault/translations/en.json create mode 100644 tests/components/renault/__init__.py create mode 100644 tests/components/renault/const.py create mode 100644 tests/components/renault/test_config_flow.py create mode 100644 tests/components/renault/test_init.py create mode 100644 tests/components/renault/test_sensor.py create mode 100644 tests/fixtures/renault/battery_status_charging.json create mode 100644 tests/fixtures/renault/battery_status_not_charging.json create mode 100644 tests/fixtures/renault/charge_mode_always.json create mode 100644 tests/fixtures/renault/charge_mode_schedule.json create mode 100644 tests/fixtures/renault/cockpit_ev.json create mode 100644 tests/fixtures/renault/cockpit_fuel.json create mode 100644 tests/fixtures/renault/hvac_status.json create mode 100644 tests/fixtures/renault/vehicle_captur_fuel.json create mode 100644 tests/fixtures/renault/vehicle_captur_phev.json create mode 100644 tests/fixtures/renault/vehicle_zoe_40.json create mode 100644 tests/fixtures/renault/vehicle_zoe_50.json diff --git a/.strict-typing b/.strict-typing index e32af5db563..6066c158b99 100644 --- a/.strict-typing +++ b/.strict-typing @@ -81,6 +81,7 @@ homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics homeassistant.components.remote.* +homeassistant.components.renault.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.scene.* homeassistant.components.select.* diff --git a/CODEOWNERS b/CODEOWNERS index bfd74e97f71..c4cb6d242d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/recollect_waste/* @bachya homeassistant/components/rejseplanen/* @DarkFox +homeassistant/components/renault/* @epenet homeassistant/components/repetier/* @MTrab homeassistant/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py new file mode 100644 index 00000000000..80433b2106e --- /dev/null +++ b/homeassistant/components/renault/__init__.py @@ -0,0 +1,45 @@ +"""Support for Renault devices.""" +import aiohttp + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_LOCALE, DOMAIN, PLATFORMS +from .renault_hub import RenaultHub + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Load a config entry.""" + renault_hub = RenaultHub(hass, config_entry.data[CONF_LOCALE]) + try: + login_success = await renault_hub.attempt_login( + config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + ) + except aiohttp.ClientConnectionError as exc: + raise ConfigEntryNotReady() from exc + + if not login_success: + return False + + hass.data.setdefault(DOMAIN, {}) + await renault_hub.async_initialise(config_entry) + + hass.data[DOMAIN][config_entry.unique_id] = renault_hub + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.unique_id) + + return unload_ok diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py new file mode 100644 index 00000000000..09a69f1f95f --- /dev/null +++ b/homeassistant/components/renault/config_flow.py @@ -0,0 +1,92 @@ +"""Config flow to configure Renault component.""" +from __future__ import annotations + +from typing import Any + +from renault_api.const import AVAILABLE_LOCALES +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, DOMAIN +from .renault_hub import RenaultHub + + +class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Renault config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the Renault config flow.""" + self.renault_config: dict[str, Any] = {} + self.renault_hub: RenaultHub | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a Renault config flow start. + + Ask the user for API keys. + """ + if user_input: + locale = user_input[CONF_LOCALE] + self.renault_config.update(user_input) + self.renault_config.update(AVAILABLE_LOCALES[locale]) + self.renault_hub = RenaultHub(self.hass, locale) + if not await self.renault_hub.attempt_login( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ): + return self._show_user_form({"base": "invalid_credentials"}) + return await self.async_step_kamereon() + return self._show_user_form() + + def _show_user_form(self, errors: dict[str, Any] | None = None) -> FlowResult: + """Show the API keys form.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_LOCALE): vol.In(AVAILABLE_LOCALES.keys()), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors or {}, + ) + + async def async_step_kamereon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select Kamereon account.""" + if user_input: + await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID]) + self._abort_if_unique_id_configured() + + self.renault_config.update(user_input) + return self.async_create_entry( + title=user_input[CONF_KAMEREON_ACCOUNT_ID], data=self.renault_config + ) + + assert self.renault_hub + accounts = await self.renault_hub.get_account_ids() + if len(accounts) == 0: + return self.async_abort(reason="kamereon_no_account") + if len(accounts) == 1: + await self.async_set_unique_id(accounts[0]) + self._abort_if_unique_id_configured() + + self.renault_config[CONF_KAMEREON_ACCOUNT_ID] = accounts[0] + return self.async_create_entry( + title=self.renault_config[CONF_KAMEREON_ACCOUNT_ID], + data=self.renault_config, + ) + + return self.async_show_form( + step_id="kamereon", + data_schema=vol.Schema( + {vol.Required(CONF_KAMEREON_ACCOUNT_ID): vol.In(accounts)} + ), + ) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py new file mode 100644 index 00000000000..51f6c10c6f1 --- /dev/null +++ b/homeassistant/components/renault/const.py @@ -0,0 +1,15 @@ +"""Constants for the Renault component.""" +DOMAIN = "renault" + +CONF_LOCALE = "locale" +CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" + +DEFAULT_SCAN_INTERVAL = 300 # 5 minutes + +PLATFORMS = [ + "sensor", +] + +DEVICE_CLASS_PLUG_STATE = "renault__plug_state" +DEVICE_CLASS_CHARGE_STATE = "renault__charge_state" +DEVICE_CLASS_CHARGE_MODE = "renault__charge_mode" diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json new file mode 100644 index 00000000000..118848ad6dd --- /dev/null +++ b/homeassistant/components/renault/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "renault", + "name": "Renault", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/renault", + "requirements": [ + "renault-api==0.1.4" + ], + "codeowners": [ + "@epenet" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py new file mode 100644 index 00000000000..b47a8507030 --- /dev/null +++ b/homeassistant/components/renault/renault_coordinator.py @@ -0,0 +1,72 @@ +"""Proxy to handle account communication with Renault servers.""" +from __future__ import annotations + +from collections.abc import Awaitable +from datetime import timedelta +import logging +from typing import Callable, TypeVar + +from renault_api.kamereon.exceptions import ( + AccessDeniedException, + KamereonResponseException, + NotSupportedException, +) + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +T = TypeVar("T") + + +class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): + """Handle vehicle communication with Renault servers.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + update_interval: timedelta, + update_method: Callable[[], Awaitable[T]], + ) -> None: + """Initialise coordinator.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + update_method=update_method, + ) + self.access_denied = False + self.not_supported = False + + async def _async_update_data(self) -> T: + """Fetch the latest data from the source.""" + if self.update_method is None: + raise NotImplementedError("Update method not implemented") + try: + return await self.update_method() + except AccessDeniedException as err: + # Disable because the account is not allowed to access this Renault endpoint. + self.update_interval = None + self.access_denied = True + raise UpdateFailed(f"This endpoint is denied: {err}") from err + + except NotSupportedException as err: + # Disable because the vehicle does not support this Renault endpoint. + self.update_interval = None + self.not_supported = True + raise UpdateFailed(f"This endpoint is not supported: {err}") from err + + except KamereonResponseException as err: + # Other Renault errors. + raise UpdateFailed(f"Error communicating with API: {err}") from err + + async def async_config_entry_first_refresh(self) -> None: + """Refresh data for the first time when a config entry is setup. + + Contrary to base implementation, we are not raising ConfigEntryNotReady + but only updating the `access_denied` and `not_supported` flags. + """ + await self._async_refresh(log_failures=False, raise_on_auth_failed=True) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py new file mode 100644 index 00000000000..9188a1f0757 --- /dev/null +++ b/homeassistant/components/renault/renault_entities.py @@ -0,0 +1,103 @@ +"""Base classes for Renault entities.""" +from __future__ import annotations + +from typing import Any, Generic, Optional, TypeVar + +from renault_api.kamereon.enums import ChargeState, PlugState +from renault_api.kamereon.models import ( + KamereonVehicleBatteryStatusData, + KamereonVehicleChargeModeData, + KamereonVehicleCockpitData, + KamereonVehicleHvacStatusData, +) + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import slugify + +from .renault_vehicle import RenaultVehicleProxy + +ATTR_LAST_UPDATE = "last_update" + +T = TypeVar("T") + + +class RenaultDataEntity(Generic[T], CoordinatorEntity[Optional[T]], Entity): + """Implementation of a Renault entity with a data coordinator.""" + + def __init__( + self, vehicle: RenaultVehicleProxy, entity_type: str, coordinator_key: str + ) -> None: + """Initialise entity.""" + super().__init__(vehicle.coordinators[coordinator_key]) + self.vehicle = vehicle + self._entity_type = entity_type + self._attr_device_info = self.vehicle.device_info + self._attr_name = entity_type + self._attr_unique_id = slugify( + f"{self.vehicle.details.vin}-{self._entity_type}" + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + # Data can succeed, but be empty + return super().available and self.coordinator.data is not None + + @property + def data(self) -> T | None: + """Return collected data.""" + return self.coordinator.data + + +class RenaultBatteryDataEntity(RenaultDataEntity[KamereonVehicleBatteryStatusData]): + """Implementation of a Renault entity with battery coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "battery") + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of this entity.""" + last_update = self.data.timestamp if self.data else None + return {ATTR_LAST_UPDATE: last_update} + + @property + def is_charging(self) -> bool: + """Return charge state as boolean.""" + return ( + self.data is not None + and self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS + ) + + @property + def is_plugged_in(self) -> bool: + """Return plug state as boolean.""" + return ( + self.data is not None and self.data.get_plug_status() == PlugState.PLUGGED + ) + + +class RenaultChargeModeDataEntity(RenaultDataEntity[KamereonVehicleChargeModeData]): + """Implementation of a Renault entity with charge_mode coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "charge_mode") + + +class RenaultCockpitDataEntity(RenaultDataEntity[KamereonVehicleCockpitData]): + """Implementation of a Renault entity with cockpit coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "cockpit") + + +class RenaultHVACDataEntity(RenaultDataEntity[KamereonVehicleHvacStatusData]): + """Implementation of a Renault entity with hvac_status coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "hvac_status") diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py new file mode 100644 index 00000000000..51e356934bb --- /dev/null +++ b/homeassistant/components/renault/renault_hub.py @@ -0,0 +1,78 @@ +"""Proxy to handle account communication with Renault servers.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.renault_account import RenaultAccount +from renault_api.renault_client import RenaultClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_KAMEREON_ACCOUNT_ID, DEFAULT_SCAN_INTERVAL +from .renault_vehicle import RenaultVehicleProxy + +LOGGER = logging.getLogger(__name__) + + +class RenaultHub: + """Handle account communication with Renault servers.""" + + def __init__(self, hass: HomeAssistant, locale: str) -> None: + """Initialise proxy.""" + LOGGER.debug("Creating RenaultHub") + self._hass = hass + self._client = RenaultClient( + websession=async_get_clientsession(self._hass), locale=locale + ) + self._account: RenaultAccount | None = None + self._vehicles: dict[str, RenaultVehicleProxy] = {} + + async def attempt_login(self, username: str, password: str) -> bool: + """Attempt login to Renault servers.""" + try: + await self._client.session.login(username, password) + except InvalidCredentialsException as ex: + LOGGER.error("Login to Renault failed: %s", ex.error_details) + else: + return True + return False + + async def async_initialise(self, config_entry: ConfigEntry) -> None: + """Set up proxy.""" + account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID] + scan_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL) + + self._account = await self._client.get_api_account(account_id) + vehicles = await self._account.get_vehicles() + if vehicles.vehicleLinks: + for vehicle_link in vehicles.vehicleLinks: + if vehicle_link.vin and vehicle_link.vehicleDetails: + # Generate vehicle proxy + vehicle = RenaultVehicleProxy( + hass=self._hass, + vehicle=await self._account.get_api_vehicle(vehicle_link.vin), + details=vehicle_link.vehicleDetails, + scan_interval=scan_interval, + ) + await vehicle.async_initialise() + self._vehicles[vehicle_link.vin] = vehicle + + async def get_account_ids(self) -> list[str]: + """Get Kamereon account ids.""" + accounts = [] + for account in await self._client.get_api_accounts(): + vehicles = await account.get_vehicles() + + # Only add the account if it has linked vehicles. + if vehicles.vehicleLinks: + accounts.append(account.account_id) + return accounts + + @property + def vehicles(self) -> dict[str, RenaultVehicleProxy]: + """Get list of vehicles.""" + return self._vehicles diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py new file mode 100644 index 00000000000..09e3de9adab --- /dev/null +++ b/homeassistant/components/renault/renault_vehicle.py @@ -0,0 +1,146 @@ +"""Proxy to handle account communication with Renault servers.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging +from typing import cast + +from renault_api.kamereon import models +from renault_api.renault_vehicle import RenaultVehicle + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN +from .renault_coordinator import RenaultDataUpdateCoordinator + +LOGGER = logging.getLogger(__name__) + + +class RenaultVehicleProxy: + """Handle vehicle communication with Renault servers.""" + + def __init__( + self, + hass: HomeAssistant, + vehicle: RenaultVehicle, + details: models.KamereonVehicleDetails, + scan_interval: timedelta, + ) -> None: + """Initialise vehicle proxy.""" + self.hass = hass + self._vehicle = vehicle + self._details = details + self._device_info: DeviceInfo = { + "identifiers": {(DOMAIN, cast(str, details.vin))}, + "manufacturer": (details.get_brand_label() or "").capitalize(), + "model": (details.get_model_label() or "").capitalize(), + "name": details.registrationNumber or "", + "sw_version": details.get_model_code() or "", + } + self.coordinators: dict[str, RenaultDataUpdateCoordinator] = {} + self.hvac_target_temperature = 21 + self._scan_interval = scan_interval + + @property + def details(self) -> models.KamereonVehicleDetails: + """Return the specs of the vehicle.""" + return self._details + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + return self._device_info + + async def async_initialise(self) -> None: + """Load available sensors.""" + if await self.endpoint_available("cockpit"): + self.coordinators["cockpit"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} cockpit", + update_method=self.get_cockpit, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + if await self.endpoint_available("hvac-status"): + self.coordinators["hvac_status"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} hvac_status", + update_method=self.get_hvac_status, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + if self.details.uses_electricity(): + if await self.endpoint_available("battery-status"): + self.coordinators["battery"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} battery", + update_method=self.get_battery_status, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + if await self.endpoint_available("charge-mode"): + self.coordinators["charge_mode"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} charge_mode", + update_method=self.get_charge_mode, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + # Check all coordinators + await asyncio.gather( + *( + coordinator.async_config_entry_first_refresh() + for coordinator in self.coordinators.values() + ) + ) + for key in list(self.coordinators): + # list() to avoid Runtime iteration error + coordinator = self.coordinators[key] + if coordinator.not_supported: + # Remove endpoint as it is not supported for this vehicle. + LOGGER.error( + "Ignoring endpoint %s as it is not supported for this vehicle: %s", + coordinator.name, + coordinator.last_exception, + ) + del self.coordinators[key] + elif coordinator.access_denied: + # Remove endpoint as it is denied for this vehicle. + LOGGER.error( + "Ignoring endpoint %s as it is denied for this vehicle: %s", + coordinator.name, + coordinator.last_exception, + ) + del self.coordinators[key] + + async def endpoint_available(self, endpoint: str) -> bool: + """Ensure the endpoint is available to avoid unnecessary queries.""" + return await self._vehicle.supports_endpoint( + endpoint + ) and await self._vehicle.has_contract_for_endpoint(endpoint) + + async def get_battery_status(self) -> models.KamereonVehicleBatteryStatusData: + """Get battery status information from vehicle.""" + return await self._vehicle.get_battery_status() + + async def get_charge_mode(self) -> models.KamereonVehicleChargeModeData: + """Get charge mode information from vehicle.""" + return await self._vehicle.get_charge_mode() + + async def get_cockpit(self) -> models.KamereonVehicleCockpitData: + """Get cockpit information from vehicle.""" + return await self._vehicle.get_cockpit() + + async def get_hvac_status(self) -> models.KamereonVehicleHvacStatusData: + """Get hvac status information from vehicle.""" + return await self._vehicle.get_hvac_status() diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py new file mode 100644 index 00000000000..8403a04d001 --- /dev/null +++ b/homeassistant/components/renault/sensor.py @@ -0,0 +1,277 @@ +"""Support for Renault sensors.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + TEMP_CELSIUS, + TIME_MINUTES, + VOLUME_LITERS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util import slugify + +from .const import ( + DEVICE_CLASS_CHARGE_MODE, + DEVICE_CLASS_CHARGE_STATE, + DEVICE_CLASS_PLUG_STATE, + DOMAIN, +) +from .renault_entities import ( + RenaultBatteryDataEntity, + RenaultChargeModeDataEntity, + RenaultCockpitDataEntity, + RenaultDataEntity, + RenaultHVACDataEntity, +) +from .renault_hub import RenaultHub +from .renault_vehicle import RenaultVehicleProxy + +ATTR_BATTERY_AVAILABLE_ENERGY = "battery_available_energy" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Renault entities from config entry.""" + proxy: RenaultHub = hass.data[DOMAIN][config_entry.unique_id] + entities = await get_entities(proxy) + async_add_entities(entities) + + +async def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]: + """Create Renault entities for all vehicles.""" + entities = [] + for vehicle in proxy.vehicles.values(): + entities.extend(await get_vehicle_entities(vehicle)) + return entities + + +async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: + """Create Renault entities for single vehicle.""" + entities: list[RenaultDataEntity] = [] + if "cockpit" in vehicle.coordinators: + entities.append(RenaultMileageSensor(vehicle, "Mileage")) + if vehicle.details.uses_fuel(): + entities.append(RenaultFuelAutonomySensor(vehicle, "Fuel Autonomy")) + entities.append(RenaultFuelQuantitySensor(vehicle, "Fuel Quantity")) + if "hvac_status" in vehicle.coordinators: + entities.append(RenaultOutsideTemperatureSensor(vehicle, "Outside Temperature")) + if "battery" in vehicle.coordinators: + entities.append(RenaultBatteryLevelSensor(vehicle, "Battery Level")) + entities.append(RenaultChargeStateSensor(vehicle, "Charge State")) + entities.append( + RenaultChargingRemainingTimeSensor(vehicle, "Charging Remaining Time") + ) + entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power")) + entities.append(RenaultPlugStateSensor(vehicle, "Plug State")) + entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy")) + entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature")) + if "charge_mode" in vehicle.coordinators: + entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode")) + return entities + + +class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity): + """Battery autonomy sensor.""" + + _attr_icon = "mdi:ev-station" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.batteryAutonomy if self.data else None + + +class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): + """Battery Level sensor.""" + + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.batteryLevel if self.data else None + + @property + def icon(self) -> str: + """Icon handling.""" + return icon_for_battery_level( + battery_level=self.state, charging=self.is_charging + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of this entity.""" + attrs = super().extra_state_attributes + attrs[ATTR_BATTERY_AVAILABLE_ENERGY] = ( + self.data.batteryAvailableEnergy if self.data else None + ) + return attrs + + +class RenaultBatteryTemperatureSensor(RenaultBatteryDataEntity, SensorEntity): + """Battery Temperature sensor.""" + + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.batteryTemperature if self.data else None + + +class RenaultChargeModeSensor(RenaultChargeModeDataEntity, SensorEntity): + """Charge Mode sensor.""" + + _attr_device_class = DEVICE_CLASS_CHARGE_MODE + + @property + def state(self) -> str | None: + """Return the state of this entity.""" + return self.data.chargeMode if self.data else None + + @property + def icon(self) -> str: + """Icon handling.""" + if self.data and self.data.chargeMode == "schedule_mode": + return "mdi:calendar-clock" + return "mdi:calendar-remove" + + +class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity): + """Charge State sensor.""" + + _attr_device_class = DEVICE_CLASS_CHARGE_STATE + + @property + def state(self) -> str | None: + """Return the state of this entity.""" + charging_status = self.data.get_charging_status() if self.data else None + return slugify(charging_status.name) if charging_status is not None else None + + @property + def icon(self) -> str: + """Icon handling.""" + return "mdi:flash" if self.is_charging else "mdi:flash-off" + + +class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity): + """Charging Remaining Time sensor.""" + + _attr_icon = "mdi:timer" + _attr_unit_of_measurement = TIME_MINUTES + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.chargingRemainingTime if self.data else None + + +class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity): + """Charging Power sensor.""" + + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_unit_of_measurement = POWER_KILO_WATT + + @property + def state(self) -> float | None: + """Return the state of this entity.""" + if not self.data or self.data.chargingInstantaneousPower is None: + return None + if self.vehicle.details.reports_charging_power_in_watts(): + # Need to convert to kilowatts + return self.data.chargingInstantaneousPower / 1000 + return self.data.chargingInstantaneousPower + + +class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity): + """Fuel autonomy sensor.""" + + _attr_icon = "mdi:gas-station" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return ( + round(self.data.fuelAutonomy) + if self.data and self.data.fuelAutonomy is not None + else None + ) + + +class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): + """Fuel quantity sensor.""" + + _attr_icon = "mdi:fuel" + _attr_unit_of_measurement = VOLUME_LITERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return ( + round(self.data.fuelQuantity) + if self.data and self.data.fuelQuantity is not None + else None + ) + + +class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): + """Mileage sensor.""" + + _attr_icon = "mdi:sign-direction" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return ( + round(self.data.totalMileage) + if self.data and self.data.totalMileage is not None + else None + ) + + +class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity): + """HVAC Outside Temperature sensor.""" + + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + + @property + def state(self) -> float | None: + """Return the state of this entity.""" + return self.data.externalTemperature if self.data else None + + +class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity): + """Plug State sensor.""" + + _attr_device_class = DEVICE_CLASS_PLUG_STATE + + @property + def state(self) -> str | None: + """Return the state of this entity.""" + plug_status = self.data.get_plug_status() if self.data else None + return slugify(plug_status.name) if plug_status is not None else None + + @property + def icon(self) -> str: + """Icon handling.""" + return "mdi:power-plug" if self.is_plugged_in else "mdi:power-plug-off" diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json new file mode 100644 index 00000000000..942c8b4a06c --- /dev/null +++ b/homeassistant/components/renault/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "kamereon_no_account": "Unable to find Kamereon account." + }, + "error": { + "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Select Kamereon account id" + }, + "user": { + "data": { + "locale": "Locale", + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "Set Renault credentials" + } + } + } +} diff --git a/homeassistant/components/renault/translations/en.json b/homeassistant/components/renault/translations/en.json new file mode 100644 index 00000000000..bb65493a3b3 --- /dev/null +++ b/homeassistant/components/renault/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Account already configured", + "kamereon_no_account": "Unable to find Kamereon account." + }, + "error": { + "invalid_credentials": "Invalid credentials." + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Select Kamereon account id" + }, + "user": { + "data": { + "locale": "Locale", + "username": "Email", + "password": "Password" + }, + "title": "Set Renault credentials" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a6c97cbbab3..89b1a9ba8ae 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -216,6 +216,7 @@ FLOWS = [ "rachio", "rainmachine", "recollect_waste", + "renault", "rfxtrx", "ring", "risco", diff --git a/mypy.ini b/mypy.ini index 32800994e42..e38897bf303 100644 --- a/mypy.ini +++ b/mypy.ini @@ -902,6 +902,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.renault.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rituals_perfume_genie.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 0acdff35fb9..bfa61df2f9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2015,6 +2015,9 @@ raspyrfm-client==1.2.8 # homeassistant.components.rainmachine regenmaschine==3.1.5 +# homeassistant.components.renault +renault-api==0.1.4 + # homeassistant.components.python_script restrictedpython==5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b9513a7e54..7d2e39327df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1109,6 +1109,9 @@ rachiopy==1.0.3 # homeassistant.components.rainmachine regenmaschine==3.1.5 +# homeassistant.components.renault +renault-api==0.1.4 + # homeassistant.components.python_script restrictedpython==5.1 diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py new file mode 100644 index 00000000000..e4edc3b8539 --- /dev/null +++ b/tests/components/renault/__init__.py @@ -0,0 +1,159 @@ +"""Tests for the Renault integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any +from unittest.mock import patch + +from renault_api.kamereon import models, schemas +from renault_api.renault_vehicle import RenaultVehicle + +from homeassistant.components.renault.const import ( + CONF_KAMEREON_ACCOUNT_ID, + CONF_LOCALE, + DOMAIN, +) +from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from .const import MOCK_VEHICLES + +from tests.common import MockConfigEntry, load_fixture + + +async def setup_renault_integration(hass: HomeAssistant): + """Create the Renault integration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source="user", + data={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + CONF_KAMEREON_ACCOUNT_ID: "account_id_2", + }, + unique_id="account_id_2", + options={}, + entry_id="1", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.renault.RenaultHub.attempt_login", return_value=True + ), patch("homeassistant.components.renault.RenaultHub.async_initialise"): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +def get_fixtures(vehicle_type: str) -> dict[str, Any]: + """Create a vehicle proxy for testing.""" + mock_vehicle = MOCK_VEHICLES[vehicle_type] + return { + "battery_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}") + if "battery_status" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema), + "charge_mode": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}") + if "charge_mode" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleChargeModeDataSchema), + "cockpit": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}") + if "cockpit" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleCockpitDataSchema), + "hvac_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['hvac_status']}") + if "hvac_status" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), + } + + +async def create_vehicle_proxy( + hass: HomeAssistant, vehicle_type: str +) -> RenaultVehicleProxy: + """Create a vehicle proxy for testing.""" + mock_vehicle = MOCK_VEHICLES[vehicle_type] + mock_fixtures = get_fixtures(vehicle_type) + + vehicles_response: models.KamereonVehiclesResponse = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ) + vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails + vehicle = RenaultVehicle( + vehicles_response.accountId, + vehicle_details.vin, + websession=aiohttp_client.async_get_clientsession(hass), + ) + + vehicle_proxy = RenaultVehicleProxy( + hass, vehicle, vehicle_details, timedelta(seconds=300) + ) + with patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + side_effect=mock_vehicle["endpoints_available"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ): + await vehicle_proxy.async_initialise() + return vehicle_proxy + + +async def create_vehicle_proxy_with_side_effect( + hass: HomeAssistant, vehicle_type: str, side_effect: Any +) -> RenaultVehicleProxy: + """Create a vehicle proxy for testing unavailable entities.""" + mock_vehicle = MOCK_VEHICLES[vehicle_type] + + vehicles_response: models.KamereonVehiclesResponse = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ) + vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails + vehicle = RenaultVehicle( + vehicles_response.accountId, + vehicle_details.vin, + websession=aiohttp_client.async_get_clientsession(hass), + ) + + vehicle_proxy = RenaultVehicleProxy( + hass, vehicle, vehicle_details, timedelta(seconds=300) + ) + with patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + side_effect=mock_vehicle["endpoints_available"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + side_effect=side_effect, + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + side_effect=side_effect, + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + side_effect=side_effect, + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + side_effect=side_effect, + ): + await vehicle_proxy.async_initialise() + return vehicle_proxy diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py new file mode 100644 index 00000000000..be2adafd7be --- /dev/null +++ b/tests/components/renault/const.py @@ -0,0 +1,328 @@ +"""Constants for the Renault integration tests.""" +from homeassistant.components.renault.const import ( + CONF_KAMEREON_ACCOUNT_ID, + CONF_LOCALE, + DEVICE_CLASS_CHARGE_MODE, + DEVICE_CLASS_CHARGE_STATE, + DEVICE_CLASS_PLUG_STATE, + DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, + TIME_MINUTES, + VOLUME_LITERS, +) + +# Mock config data to be used across multiple tests +MOCK_CONFIG = { + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + CONF_KAMEREON_ACCOUNT_ID: "account_id_1", + CONF_LOCALE: "fr_FR", +} + +MOCK_VEHICLES = { + "zoe_40": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777999")}, + "manufacturer": "Renault", + "model": "Zoe", + "name": "REG-NUMBER", + "sw_version": "X101VE", + }, + "endpoints_available": [ + True, # cockpit + True, # hvac-status + True, # battery-status + True, # charge-mode + ], + "endpoints": { + "battery_status": "battery_status_charging.json", + "charge_mode": "charge_mode_always.json", + "cockpit": "cockpit_ev.json", + "hvac_status": "hvac_status.json", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.battery_autonomy", + "unique_id": "vf1aaaaa555777999_battery_autonomy", + "result": "141", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.battery_level", + "unique_id": "vf1aaaaa555777999_battery_level", + "result": "60", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_BATTERY, + }, + { + "entity_id": "sensor.battery_temperature", + "unique_id": "vf1aaaaa555777999_battery_temperature", + "result": "20", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.charge_mode", + "unique_id": "vf1aaaaa555777999_charge_mode", + "result": "always", + "class": DEVICE_CLASS_CHARGE_MODE, + }, + { + "entity_id": "sensor.charge_state", + "unique_id": "vf1aaaaa555777999_charge_state", + "result": "charge_in_progress", + "class": DEVICE_CLASS_CHARGE_STATE, + }, + { + "entity_id": "sensor.charging_power", + "unique_id": "vf1aaaaa555777999_charging_power", + "result": "0.027", + "unit": POWER_KILO_WATT, + "class": DEVICE_CLASS_ENERGY, + }, + { + "entity_id": "sensor.charging_remaining_time", + "unique_id": "vf1aaaaa555777999_charging_remaining_time", + "result": "145", + "unit": TIME_MINUTES, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777999_mileage", + "result": "49114", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.outside_temperature", + "unique_id": "vf1aaaaa555777999_outside_temperature", + "result": "8.0", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.plug_state", + "unique_id": "vf1aaaaa555777999_plug_state", + "result": "plugged", + "class": DEVICE_CLASS_PLUG_STATE, + }, + ], + }, + "zoe_50": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777999")}, + "manufacturer": "Renault", + "model": "Zoe", + "name": "REG-NUMBER", + "sw_version": "X102VE", + }, + "endpoints_available": [ + True, # cockpit + False, # hvac-status + True, # battery-status + True, # charge-mode + ], + "endpoints": { + "battery_status": "battery_status_not_charging.json", + "charge_mode": "charge_mode_schedule.json", + "cockpit": "cockpit_ev.json", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.battery_autonomy", + "unique_id": "vf1aaaaa555777999_battery_autonomy", + "result": "128", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.battery_level", + "unique_id": "vf1aaaaa555777999_battery_level", + "result": "50", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_BATTERY, + }, + { + "entity_id": "sensor.battery_temperature", + "unique_id": "vf1aaaaa555777999_battery_temperature", + "result": STATE_UNKNOWN, + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.charge_mode", + "unique_id": "vf1aaaaa555777999_charge_mode", + "result": "schedule_mode", + "class": DEVICE_CLASS_CHARGE_MODE, + }, + { + "entity_id": "sensor.charge_state", + "unique_id": "vf1aaaaa555777999_charge_state", + "result": "charge_error", + "class": DEVICE_CLASS_CHARGE_STATE, + }, + { + "entity_id": "sensor.charging_power", + "unique_id": "vf1aaaaa555777999_charging_power", + "result": STATE_UNKNOWN, + "unit": POWER_KILO_WATT, + "class": DEVICE_CLASS_ENERGY, + }, + { + "entity_id": "sensor.charging_remaining_time", + "unique_id": "vf1aaaaa555777999_charging_remaining_time", + "result": STATE_UNKNOWN, + "unit": TIME_MINUTES, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777999_mileage", + "result": "49114", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.plug_state", + "unique_id": "vf1aaaaa555777999_plug_state", + "result": "unplugged", + "class": DEVICE_CLASS_PLUG_STATE, + }, + ], + }, + "captur_phev": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777123")}, + "manufacturer": "Renault", + "model": "Captur ii", + "name": "REG-NUMBER", + "sw_version": "XJB1SU", + }, + "endpoints_available": [ + True, # cockpit + False, # hvac-status + True, # battery-status + True, # charge-mode + ], + "endpoints": { + "battery_status": "battery_status_charging.json", + "charge_mode": "charge_mode_always.json", + "cockpit": "cockpit_fuel.json", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.battery_autonomy", + "unique_id": "vf1aaaaa555777123_battery_autonomy", + "result": "141", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.battery_level", + "unique_id": "vf1aaaaa555777123_battery_level", + "result": "60", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_BATTERY, + }, + { + "entity_id": "sensor.battery_temperature", + "unique_id": "vf1aaaaa555777123_battery_temperature", + "result": "20", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.charge_mode", + "unique_id": "vf1aaaaa555777123_charge_mode", + "result": "always", + "class": DEVICE_CLASS_CHARGE_MODE, + }, + { + "entity_id": "sensor.charge_state", + "unique_id": "vf1aaaaa555777123_charge_state", + "result": "charge_in_progress", + "class": DEVICE_CLASS_CHARGE_STATE, + }, + { + "entity_id": "sensor.charging_power", + "unique_id": "vf1aaaaa555777123_charging_power", + "result": "27.0", + "unit": POWER_KILO_WATT, + "class": DEVICE_CLASS_ENERGY, + }, + { + "entity_id": "sensor.charging_remaining_time", + "unique_id": "vf1aaaaa555777123_charging_remaining_time", + "result": "145", + "unit": TIME_MINUTES, + }, + { + "entity_id": "sensor.fuel_autonomy", + "unique_id": "vf1aaaaa555777123_fuel_autonomy", + "result": "35", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.fuel_quantity", + "unique_id": "vf1aaaaa555777123_fuel_quantity", + "result": "3", + "unit": VOLUME_LITERS, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777123_mileage", + "result": "5567", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.plug_state", + "unique_id": "vf1aaaaa555777123_plug_state", + "result": "plugged", + "class": DEVICE_CLASS_PLUG_STATE, + }, + ], + }, + "captur_fuel": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777123")}, + "manufacturer": "Renault", + "model": "Captur ii", + "name": "REG-NUMBER", + "sw_version": "XJB1SU", + }, + "endpoints_available": [ + True, # cockpit + False, # hvac-status + # Ignore, # battery-status + # Ignore, # charge-mode + ], + "endpoints": {"cockpit": "cockpit_fuel.json"}, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.fuel_autonomy", + "unique_id": "vf1aaaaa555777123_fuel_autonomy", + "result": "35", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.fuel_quantity", + "unique_id": "vf1aaaaa555777123_fuel_quantity", + "result": "3", + "unit": VOLUME_LITERS, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777123_mileage", + "result": "5567", + "unit": LENGTH_KILOMETERS, + }, + ], + }, +} diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py new file mode 100644 index 00000000000..c8b9c8c3e12 --- /dev/null +++ b/tests/components/renault/test_config_flow.py @@ -0,0 +1,137 @@ +"""Test the Renault config flow.""" +from unittest.mock import AsyncMock, PropertyMock, patch + +from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.kamereon import schemas + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.renault.const import ( + CONF_KAMEREON_ACCOUNT_ID, + CONF_LOCALE, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture + + +async def test_config_flow_single_account(hass: HomeAssistant): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Failed credentials + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + renault_account = AsyncMock() + type(renault_account).account_id = PropertyMock(return_value="account_id_1") + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + # Account list single + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="123" + ), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_1" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" + assert result["data"][CONF_LOCALE] == "fr_FR" + + +async def test_config_flow_no_account(hass: HomeAssistant): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Account list empty + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "homeassistant.components.renault.config_flow.RenaultHub.get_account_ids", + return_value=[], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "kamereon_no_account" + + +async def test_config_flow_multiple_accounts(hass: HomeAssistant): + """Test what happens if multiple Kamereon accounts are available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Multiple accounts + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "homeassistant.components.renault.config_flow.RenaultHub.get_account_ids", + return_value=["account_id_1", "account_id_2"], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "kamereon" + + # Account selected + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_2" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" + assert result["data"][CONF_LOCALE] == "fr_FR" diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py new file mode 100644 index 00000000000..974155c3df9 --- /dev/null +++ b/tests/components/renault/test_init.py @@ -0,0 +1,85 @@ +"""Tests for Renault setup process.""" +from unittest.mock import AsyncMock, patch + +import aiohttp +import pytest +from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.kamereon import schemas + +from homeassistant.components.renault import ( + RenaultHub, + async_setup_entry, + async_unload_entry, +) +from homeassistant.components.renault.const import DOMAIN +from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import MOCK_CONFIG + +from tests.common import MockConfigEntry, load_fixture + + +async def test_setup_unload_and_reload_entry(hass): + """Test entry setup and unload.""" + # Create a mock entry so we don't have to go through config flow + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 + ) + renault_account = AsyncMock() + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ): + # Set up the entry and assert that the values set during setup are where we expect + # them to be. + assert await async_setup_entry(hass, config_entry) + assert DOMAIN in hass.data and config_entry.unique_id in hass.data[DOMAIN] + assert isinstance(hass.data[DOMAIN][config_entry.unique_id], RenaultHub) + + renault_hub: RenaultHub = hass.data[DOMAIN][config_entry.unique_id] + assert len(renault_hub.vehicles) == 1 + assert isinstance( + renault_hub.vehicles["VF1AAAAA555777999"], RenaultVehicleProxy + ) + + # Unload the entry and verify that the data has been removed + assert await async_unload_entry(hass, config_entry) + assert config_entry.unique_id not in hass.data[DOMAIN] + + +async def test_setup_entry_bad_password(hass): + """Test entry setup and unload.""" + # Create a mock entry so we don't have to go through config flow + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 + ) + + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + ): + # Set up the entry and assert that the values set during setup are where we expect + # them to be. + assert not await async_setup_entry(hass, config_entry) + + +async def test_setup_entry_exception(hass): + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 + ) + + # In this case we are testing the condition where async_setup_entry raises + # ConfigEntryNotReady. + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=aiohttp.ClientConnectionError, + ), pytest.raises(ConfigEntryNotReady): + assert await async_setup_entry(hass, config_entry) diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py new file mode 100644 index 00000000000..8956fa7e7e6 --- /dev/null +++ b/tests/components/renault/test_sensor.py @@ -0,0 +1,212 @@ +"""Tests for Renault sensors.""" +from unittest.mock import PropertyMock, patch + +import pytest +from renault_api.kamereon import exceptions + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component + +from . import ( + create_vehicle_proxy, + create_vehicle_proxy_with_side_effect, + setup_renault_integration, +) +from .const import MOCK_VEHICLES + +from tests.common import mock_device_registry, mock_registry + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_sensors(hass, vehicle_type): + """Test for Renault sensors.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_proxy = await create_vehicle_proxy(hass, vehicle_type) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + assert len(device_registry.devices) == 1 + expected_device = mock_vehicle["expected_device"] + registry_entry = device_registry.async_get_device(expected_device["identifiers"]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device["identifiers"] + assert registry_entry.manufacturer == expected_device["manufacturer"] + assert registry_entry.name == expected_device["name"] + assert registry_entry.model == expected_device["model"] + assert registry_entry.sw_version == expected_device["sw_version"] + + expected_entities = mock_vehicle[SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == expected_entity["result"] + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_sensor_empty(hass, vehicle_type): + """Test for Renault sensors with empty data from Renault.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect(hass, vehicle_type, {}) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + assert len(device_registry.devices) == 1 + expected_device = mock_vehicle["expected_device"] + registry_entry = device_registry.async_get_device(expected_device["identifiers"]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device["identifiers"] + assert registry_entry.manufacturer == expected_device["manufacturer"] + assert registry_entry.name == expected_device["name"] + assert registry_entry.model == expected_device["model"] + assert registry_entry.sw_version == expected_device["sw_version"] + + expected_entities = mock_vehicle[SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_sensor_errors(hass, vehicle_type): + """Test for Renault sensors with temporary failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + invalid_upstream_exception = exceptions.InvalidUpstreamException( + "err.tech.500", + "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", + ) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + assert len(device_registry.devices) == 1 + expected_device = mock_vehicle["expected_device"] + registry_entry = device_registry.async_get_device(expected_device["identifiers"]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device["identifiers"] + assert registry_entry.manufacturer == expected_device["manufacturer"] + assert registry_entry.name == expected_device["name"] + assert registry_entry.model == expected_device["model"] + assert registry_entry.sw_version == expected_device["sw_version"] + + expected_entities = mock_vehicle[SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +async def test_sensor_access_denied(hass): + """Test for Renault sensors with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + access_denied_exception = exceptions.AccessDeniedException( + "err.func.403", + "Access is denied for this resource", + ) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect( + hass, "zoe_40", access_denied_exception + ) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + assert len(device_registry.devices) == 0 + assert len(entity_registry.entities) == 0 + + +async def test_sensor_not_supported(hass): + """Test for Renault sensors with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + not_supported_exception = exceptions.NotSupportedException( + "err.tech.501", + "This feature is not technically supported by this gateway", + ) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect( + hass, "zoe_40", not_supported_exception + ) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + assert len(device_registry.devices) == 0 + assert len(entity_registry.entities) == 0 diff --git a/tests/fixtures/renault/battery_status_charging.json b/tests/fixtures/renault/battery_status_charging.json new file mode 100644 index 00000000000..dbde4597e93 --- /dev/null +++ b/tests/fixtures/renault/battery_status_charging.json @@ -0,0 +1,18 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "timestamp": "2020-01-12T21:40:16Z", + "batteryLevel": 60, + "batteryTemperature": 20, + "batteryAutonomy": 141, + "batteryCapacity": 0, + "batteryAvailableEnergy": 31, + "plugStatus": 1, + "chargingStatus": 1.0, + "chargingRemainingTime": 145, + "chargingInstantaneousPower": 27 + } + } +} diff --git a/tests/fixtures/renault/battery_status_not_charging.json b/tests/fixtures/renault/battery_status_not_charging.json new file mode 100644 index 00000000000..750d0081ed9 --- /dev/null +++ b/tests/fixtures/renault/battery_status_not_charging.json @@ -0,0 +1,15 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "timestamp": "2020-11-17T09:06:48+01:00", + "batteryLevel": 50, + "batteryAutonomy": 128, + "batteryCapacity": 0, + "batteryAvailableEnergy": 0, + "plugStatus": 0, + "chargingStatus": -1.0 + } + } +} diff --git a/tests/fixtures/renault/charge_mode_always.json b/tests/fixtures/renault/charge_mode_always.json new file mode 100644 index 00000000000..6f146a2f72f --- /dev/null +++ b/tests/fixtures/renault/charge_mode_always.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { "chargeMode": "always" } + } +} diff --git a/tests/fixtures/renault/charge_mode_schedule.json b/tests/fixtures/renault/charge_mode_schedule.json new file mode 100644 index 00000000000..778994746ff --- /dev/null +++ b/tests/fixtures/renault/charge_mode_schedule.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { "chargeMode": "schedule_mode" } + } +} diff --git a/tests/fixtures/renault/cockpit_ev.json b/tests/fixtures/renault/cockpit_ev.json new file mode 100644 index 00000000000..c5a390f3dda --- /dev/null +++ b/tests/fixtures/renault/cockpit_ev.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "totalMileage": 49114.27 + } + } +} diff --git a/tests/fixtures/renault/cockpit_fuel.json b/tests/fixtures/renault/cockpit_fuel.json new file mode 100644 index 00000000000..575a4236c19 --- /dev/null +++ b/tests/fixtures/renault/cockpit_fuel.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777123", + "attributes": { + "fuelAutonomy": 35.0, + "fuelQuantity": 3.0, + "totalMileage": 5566.78 + } + } +} diff --git a/tests/fixtures/renault/hvac_status.json b/tests/fixtures/renault/hvac_status.json new file mode 100644 index 00000000000..f48cbae68ae --- /dev/null +++ b/tests/fixtures/renault/hvac_status.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } + } +} diff --git a/tests/fixtures/renault/vehicle_captur_fuel.json b/tests/fixtures/renault/vehicle_captur_fuel.json new file mode 100644 index 00000000000..3aa854c61ea --- /dev/null +++ b/tests/fixtures/renault/vehicle_captur_fuel.json @@ -0,0 +1,108 @@ +{ + "accountId": "account-id-1", + "country": "LU", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777123", + "status": "ACTIVE", + "linkType": "USER", + "garageBrand": "RENAULT", + "mileage": 346, + "startDate": "2020-06-12", + "createdDate": "2020-06-12T15:02:00.555432Z", + "lastModifiedDate": "2020-06-15T06:21:43.762467Z", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2020-06-15T06:20:39.107794Z", + "lastModifiedDate": "2020-06-15T06:20:39.107794Z" + }, + "vehicleDetails": { + "vin": "VF1AAAAA555777123", + "engineType": "H5H", + "engineRatio": "470", + "modelSCR": "CP1", + "deliveryCountry": { + "code": "BE", + "label": "BELGIQUE" + }, + "family": { + "code": "XJB", + "label": "FAMILLE B+X OVER", + "group": "007" + }, + "tcu": { + "code": "AIVCT", + "label": "AVEC BOITIER CONNECT AIVC", + "group": "E70" + }, + "navigationAssistanceLevel": { + "code": "", + "label": "", + "group": "" + }, + "battery": { + "code": "SANBAT", + "label": "SANS BATTERIE", + "group": "968" + }, + "radioType": { + "code": "NA406", + "label": "A-IVIMINDL, 2BO + 2BI + 2T, MICRO-DOUBLE, FM1/DAB+FM2", + "group": "425" + }, + "registrationCountry": { + "code": "BE" + }, + "brand": { + "label": "RENAULT" + }, + "model": { + "code": "XJB1SU", + "label": "CAPTUR II", + "group": "971" + }, + "gearbox": { + "code": "BVA7", + "label": "BOITE DE VITESSE AUTOMATIQUE 7 RAPPORTS", + "group": "427" + }, + "version": { + "code": "ITAMFHA 6TH" + }, + "energy": { + "code": "ESS", + "label": "ESSENCE", + "group": "019" + }, + "registrationNumber": "REG-NUMBER", + "vcd": "ADR00/DLIGM2/PGPRT2/FEUAR3/CDVIT1/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET04/DANGMO/ECOMOD/SSRCAR/AIVCT/AVGSI/TPRPE/TSGNE/2TON/ITPK7/MLEXP1/SPERTA/SSPERG/SPERTP/VOLCHA/SREACT/AVOSP1/SWALBO/DWGE01/AVC1A/1234Y/AEBS07/PRAHL/AVCAM/STANDA/XJB/HJB/EA3/MF/ESS/DG/TEMP/TR4X2/AFURGE/RVDIST/ABS/SBARTO/CA02/TOPAN/PBNCH/LAC/VSTLAR/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV02/SGAR02/BIXPE/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/SCACBA/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETIN2/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGACHA/BEL01/APL03/FSTPO/ALOUC5/CMAR3P/FIPOU2/NA406/BVA7/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRGAC/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06T/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/SSTYAD/HYB01/SSCABA/SANBAT/VEC012/XJB1SU/SSNBT/H5H", + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + } + ], + "yearsOfMaintenance": 12, + "connectivityTechnology": "NONE", + "easyConnectStore": false, + "electrical": false, + "rlinkStore": false, + "deliveryDate": "2020-06-17", + "retrievedFromDhs": false, + "engineEnergyType": "OTHER", + "radioCode": "1234" + } + } + ] +} diff --git a/tests/fixtures/renault/vehicle_captur_phev.json b/tests/fixtures/renault/vehicle_captur_phev.json new file mode 100644 index 00000000000..03066c8238f --- /dev/null +++ b/tests/fixtures/renault/vehicle_captur_phev.json @@ -0,0 +1,110 @@ +{ + "accountId": "account-id-2", + "country": "IT", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777123", + "status": "ACTIVE", + "linkType": "OWNER", + "garageBrand": "RENAULT", + "startDate": "2020-10-07", + "createdDate": "2020-10-07T09:17:44.692802Z", + "lastModifiedDate": "2021-03-28T10:44:01.139649Z", + "ownershipStartDate": "2020-09-30", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2020-10-08T17:36:39.445523Z", + "lastModifiedDate": "2020-10-08T17:36:39.445523Z" + }, + "vehicleDetails": { + "vin": "VF1AAAAA555777123", + "registrationDate": "2020-09-30", + "firstRegistrationDate": "2020-09-30", + "engineType": "H4M", + "engineRatio": "630", + "modelSCR": "", + "deliveryCountry": { + "code": "IT", + "label": "ITALY" + }, + "family": { + "code": "XJB", + "label": "B+X OVER FAMILY", + "group": "007" + }, + "tcu": { + "code": "AIVCT", + "label": "WITH AIVC CONNECTION UNIT", + "group": "E70" + }, + "navigationAssistanceLevel": { + "code": "", + "label": "", + "group": "" + }, + "battery": { + "code": "BT9AE1", + "label": "BATTERY BT9AE1", + "group": "968" + }, + "radioType": { + "code": "NA418", + "label": "FULL NAV DAB ETH - AUDI", + "group": "425" + }, + "registrationCountry": { + "code": "IT" + }, + "brand": { + "label": "RENAULT" + }, + "model": { + "code": "XJB1SU", + "label": "CAPTUR II", + "group": "971" + }, + "gearbox": { + "code": "BVH4", + "label": "HYBRID 4 SPEED GEARBOX", + "group": "427" + }, + "version": { + "code": "ITAMMHH 6UP" + }, + "energy": { + "code": "ESS", + "label": "PETROL", + "group": "019" + }, + "registrationNumber": "REG-NUMBER", + "vcd": "STANDA/XJB/HJB/EA3/MM/ESS/DG/TEMP/TR4X2/AFURGE/RV/ABS/SBARTO/CA02/TN/PBNCH/LAC/VT/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV01/SGAR02/BIYPC/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/CACBL3/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETRCR/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGSCHA/ITA01/APL03/FSTPO/ALOUC5/PART01/CMAR3P/FIPOU2/NA418/BVH4/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRFLY/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06U/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/5DHS/HYB06/010KWH/BT9AE1/VEC237/XJB1SU/NBT018/H4M/NOADR/DLIGM2/PGPRT2/FEUAR3/SCDVIT/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET05/SDANGM/ECOMOD/SSRCAR/AIVCT/AVGSI/TPQNW/TSGNE/2TON/ITPK4/MLEXP1/SPERTA/SSPERG/SPERTP/VOLNCH/SREACT/AVTSR1/SWALBO/DWGE01/AVC1A/VSPTA/1234Y/AEBS07/PRAHL/RRCAM", + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=HJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRV%2FSBARTO%2FCA02%2FTN%2FPBNCH%2FVT%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIYPC%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETRCR%2FREPNTC%2FLVAVIP%2FLVAREI%2FALOUC5%2FNA418%2FBVH4%2FECLHB4%2FRDIF10%2FCSRFLY%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB06%2FH4M%2FNOADR%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FSSCCPC%2FRCALL%2FMET05%2FSDANGM%2FSSRCAR%2FAVGSI%2FITPK4%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLNCH%2FSREACT%2FDWGE01%2FRRCAM&databaseId=b2b4fefb-d131-4f8f-9a24-4223c38bc710&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=HJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRV%2FSBARTO%2FCA02%2FTN%2FPBNCH%2FVT%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIYPC%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETRCR%2FREPNTC%2FLVAVIP%2FLVAREI%2FALOUC5%2FNA418%2FBVH4%2FECLHB4%2FRDIF10%2FCSRFLY%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB06%2FH4M%2FNOADR%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FSSCCPC%2FRCALL%2FMET05%2FSDANGM%2FSSRCAR%2FAVGSI%2FITPK4%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLNCH%2FSREACT%2FDWGE01%2FRRCAM&databaseId=b2b4fefb-d131-4f8f-9a24-4223c38bc710&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + } + ], + "yearsOfMaintenance": 12, + "connectivityTechnology": "NONE", + "easyConnectStore": false, + "electrical": false, + "rlinkStore": false, + "deliveryDate": "2020-09-30", + "retrievedFromDhs": false, + "engineEnergyType": "PHEV", + "radioCode": "1234" + } + } + ] +} diff --git a/tests/fixtures/renault/vehicle_zoe_40.json b/tests/fixtures/renault/vehicle_zoe_40.json new file mode 100644 index 00000000000..ab80d586652 --- /dev/null +++ b/tests/fixtures/renault/vehicle_zoe_40.json @@ -0,0 +1,189 @@ +{ + "accountId": "account-id-1", + "country": "FR", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777999", + "status": "ACTIVE", + "linkType": "OWNER", + "garageBrand": "RENAULT", + "annualMileage": 16000, + "mileage": 26464, + "startDate": "2017-08-07", + "createdDate": "2019-05-23T21:38:16.409008Z", + "lastModifiedDate": "2020-11-17T08:41:40.497400Z", + "ownershipStartDate": "2017-08-01", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2019-06-17T09:49:06.880627Z", + "lastModifiedDate": "2019-06-17T09:49:06.880627Z" + }, + "vehicleDetails": { + "vin": "VF1AAAAA555777999", + "registrationDate": "2017-08-01", + "firstRegistrationDate": "2017-08-01", + "engineType": "5AQ", + "engineRatio": "601", + "modelSCR": "ZOE", + "deliveryCountry": { + "code": "FR", + "label": "FRANCE" + }, + "family": { + "code": "X10", + "label": "FAMILLE X10", + "group": "007" + }, + "tcu": { + "code": "TCU0G2", + "label": "TCU VER 0 GEN 2", + "group": "E70" + }, + "navigationAssistanceLevel": { + "code": "NAV3G5", + "label": "LEVEL 3 TYPE 5 NAVIGATION", + "group": "408" + }, + "battery": { + "code": "BT4AR1", + "label": "BATTERIE BT4AR1", + "group": "968" + }, + "radioType": { + "code": "RAD37A", + "label": "RADIO 37A", + "group": "425" + }, + "registrationCountry": { + "code": "FR" + }, + "brand": { + "label": "RENAULT" + }, + "model": { + "code": "X101VE", + "label": "ZOE", + "group": "971" + }, + "gearbox": { + "code": "BVEL", + "label": "BOITE A VARIATEUR ELECTRIQUE", + "group": "427" + }, + "version": { + "code": "INT MB 10R" + }, + "energy": { + "code": "ELEC", + "label": "ELECTRIQUE", + "group": "019" + }, + "registrationNumber": "REG-NUMBER", + "vcd": "SYTINC/SKTPOU/SAND41/FDIU1/SSESM/MAPSUP/SSCALL/SAND88/SAND90/SQKDRO/SDIFPA/FACBA2/PRLEX1/SSRCAR/CABDO2/TCU0G2/SWALBO/EVTEC1/STANDA/X10/B10/EA2/MB/ELEC/DG/TEMP/TR4X2/RV/ABS/CAREG/LAC/VT003/CPE/RET03/SPROJA/RALU16/CEAVRH/AIRBA1/SERIE/DRA/DRAP08/HARM02/ATAR/TERQG/SFBANA/KM/DPRPN/AVREPL/SSDECA/ASRESP/RDAR02/ALEVA/CACBL2/SOP02C/CTHAB2/TRNOR/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/APL03/ALOUCC/CMAR3P/NAV3G5/RAD37A/BVEL/AUTAUG/RNORM/ISOFIX/EQPEUR/HRGM01/SDPCLV/TLFRAN/SPRODI/SAN613/SSAPEX/GENEV1/ELC1/SANCML/PE2012/PHAS1/SAN913/045KWH/BT4AR1/VEC153/X101VE/NBT017/5AQ", + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + }, + { + "assetType": "PDF", + "assetRole": "GUIDE", + "title": "PDF Guide", + "description": "", + "renditions": [ + { + "url": "https://cdn.group.renault.com/ren/gb/myr/assets/x101ve/manual.pdf.asset.pdf/1558704861676.pdf" + } + ] + }, + { + "assetType": "URL", + "assetRole": "GUIDE", + "title": "e-guide", + "description": "", + "renditions": [ + { + "url": "http://gb.e-guide.renault.com/eng/Zoe" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "10 Fundamentals about getting the best out of your electric vehicle", + "description": "", + "renditions": [ + { + "url": "39r6QEKcOM4" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "Automatic Climate Control", + "description": "", + "renditions": [ + { + "url": "Va2FnZFo_GE" + } + ] + }, + { + "assetType": "URL", + "assetRole": "CAR", + "title": "More videos", + "description": "", + "renditions": [ + { + "url": "https://www.youtube.com/watch?v=wfpCMkK1rKI" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "Charging the battery", + "description": "", + "renditions": [ + { + "url": "RaEad8DjUJs" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "Charging the battery at a station with a flap", + "description": "", + "renditions": [ + { + "url": "zJfd7fJWtr0" + } + ] + } + ], + "yearsOfMaintenance": 12, + "connectivityTechnology": "RLINK1", + "easyConnectStore": false, + "electrical": true, + "rlinkStore": false, + "deliveryDate": "2017-08-11", + "retrievedFromDhs": false, + "engineEnergyType": "ELEC", + "radioCode": "1234" + } + } + ] +} diff --git a/tests/fixtures/renault/vehicle_zoe_50.json b/tests/fixtures/renault/vehicle_zoe_50.json new file mode 100644 index 00000000000..560b2a2246a --- /dev/null +++ b/tests/fixtures/renault/vehicle_zoe_50.json @@ -0,0 +1,161 @@ +{ + "country": "GB", + "vehicleLinks": [ + { + "preferredDealer": { + "brand": "RENAULT", + "createdDate": "2019-05-23T20:42:01.086661Z", + "lastModifiedDate": "2019-05-23T20:42:01.086662Z", + "dealerId": "dealer-id-1" + }, + "garageBrand": "RENAULT", + "vehicleDetails": { + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLNCH%2FREACTI%2FSSAEBS%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU16%2FDRAP13%2F3ATRPH%2FTELNJ%2FALEVA%2FVLCUIR%2FRETRCR%2FRETC%2FLVAREL%2FSGSCHA%2FNA418%2FRDIF01%2FTL01A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLNCH%2FREACTI%2FSSAEBS%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU16%2FDRAP13%2F3ATRPH%2FTELNJ%2FALEVA%2FVLCUIR%2FRETRCR%2FRETC%2FLVAREL%2FSGSCHA%2FNA418%2FRDIF01%2FTL01A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + }, + { + "title": "PDF Guide", + "description": "", + "assetType": "PDF", + "assetRole": "GUIDE", + "renditions": [ + { + "url": "https://cdn.group.renault.com/ren/gb/myr/assets/x102ve/manual.pdf.asset.pdf/1558696740707.pdf" + } + ] + }, + { + "title": "e-guide", + "description": "", + "assetType": "URL", + "assetRole": "GUIDE", + "renditions": [ + { + "url": "https://gb.e-guide.renault.com/eng/Zoe-ph2" + } + ] + }, + { + "title": "All-New ZOE: Welcome to your new car", + "description": "", + "assetType": "VIDEO", + "assetRole": "CAR", + "renditions": [ + { + "url": "1OGwwmWHB6o" + } + ] + }, + { + "title": "Renault ZOE: All you need to know", + "description": "", + "assetType": "VIDEO", + "assetRole": "CAR", + "renditions": [ + { + "url": "_BVH-Rd6e5I" + } + ] + } + ], + "engineType": "5AQ", + "registrationCountry": { + "code": "FR" + }, + "radioType": { + "group": "425", + "code": "NA418", + "label": " FULL NAV DAB ETH - AUDI" + }, + "tcu": { + "group": "E70", + "code": "AIVCT", + "label": "AVEC BOITIER CONNECT AIVC" + }, + "brand": { + "label": "RENAULT" + }, + "deliveryDate": "2020-01-22", + "engineEnergyType": "ELEC", + "registrationDate": "2020-01-13", + "gearbox": { + "group": "427", + "code": "BVEL", + "label": "BOITE A VARIATEUR ELECTRIQUE" + }, + "model": { + "group": "971", + "code": "X102VE", + "label": "ZOE" + }, + "electrical": true, + "energy": { + "group": "019", + "code": "ELEC", + "label": "ELECTRIQUE" + }, + "navigationAssistanceLevel": { + "group": "408", + "code": "SAN408", + "label": "CRITERE DE CONTEXTE" + }, + "yearsOfMaintenance": 12, + "rlinkStore": false, + "radioCode": "1234", + "registrationNumber": "REG-NUMBER", + "modelSCR": "ZOE", + "easyConnectStore": false, + "engineRatio": "605", + "battery": { + "group": "968", + "code": "BT4AR1", + "label": "BATTERIE BT4AR1" + }, + "vin": "VF1AAAAA555777999", + "retrievedFromDhs": false, + "vcd": "ASCOD0/DLIGM2/SSTINC/KITPOU/SKTPGR/SSCCPC/SDPSEC/FDIU2/SSMAP/SSCALL/FACBA1/DANGMO/SSRCAR/SSCABD/AIVCT/AVGSI/ITPK4/VOLNCH/REACTI/AVOSP1/SWALBO/SSDWGE/1234Y/SSAEBS/PRAHL/RRCAM/STANDA/X10/B10/EA3/MD/ELEC/DG/TEMP/TR4X2/AFURGE/RV/ABS/CAREG/LAC/VSTLAR/CPETIR/RET03/PROJAB/RALU16/CEAVRH/ADAC/AIRBA2/SERIE/DRA/DRAP13/HARM02/3ATRPH/SGAV01/BARRAB/TELNJ/SFBANA/KM/DPRPN/AVREPL/SSDECA/ABLAV/ASRESP/ALEVA/SCACBA/SOP02C/STHPLG/SKTGRV/VLCUIR/RETRCR/TRSEV1/RETC/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/FRA01/APL03/FSTPO/ALOUC5/CMAR3P/SAN408/NA418/BVEL/AUTAUG/SPREST/RDIF01/ISOFIX/EQPEUR/HRGM01/SDPCLV/CHASTD/TL01A/SPRODI/SAN613/AIRBDE/PSMREC/ELC1/SSPTLP/SANCML/SEXTIN/PE2019/PHAS2/SAN913/THABT2/SSTYAD/SSHYB/052KWH/BT4AR1/VEC018/X102VE/NBT022/5AQ", + "firstRegistrationDate": "2020-01-13", + "deliveryCountry": { + "code": "FR", + "label": "FRANCE" + }, + "connectivityTechnology": "RLINK1", + "family": { + "group": "007", + "code": "X10", + "label": "FAMILLE X10" + }, + "version": { + "code": "INT A MD 1L" + } + }, + "status": "ACTIVE", + "createdDate": "2020-08-21T16:48:00.243967Z", + "cancellationReason": {}, + "linkType": "OWNER", + "connectedDriver": { + "role": "MAIN_DRIVER", + "lastModifiedDate": "2020-08-22T09:41:53.477398Z", + "createdDate": "2020-08-22T09:41:53.477398Z" + }, + "vin": "VF1AAAAA555777999", + "lastModifiedDate": "2020-11-29T22:01:21.162572Z", + "brand": "RENAULT", + "startDate": "2020-08-21", + "ownershipStartDate": "2020-01-13", + "ownershipEndDate": "2020-08-21" + } + ], + "accountId": "account-id-1" +} From 3265c7b8d89569eab411c4a2f9606c64a2e306da Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 28 Jul 2021 17:15:27 -0400 Subject: [PATCH 713/818] Add zwave_js.reset_meter service (#53390) * Add zwave_js.meter_reset service * fix log statement * Add endpoint attribute to service call and rename service * Make service an entity service * remove endpoint from service description --- homeassistant/components/zwave_js/const.py | 22 +++---- homeassistant/components/zwave_js/sensor.py | 38 +++++++++++- .../components/zwave_js/services.yaml | 23 ++++++++ tests/components/zwave_js/test_sensor.py | 59 +++++++++++++++++++ 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ae5607745f6..7848af146b5 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -49,24 +49,24 @@ ATTR_NODE = "node" ATTR_ZWAVE_VALUE = "zwave_value" # service constants -ATTR_NODES = "nodes" - +SERVICE_SET_VALUE = "set_value" +SERVICE_RESET_METER = "reset_meter" +SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" +SERVICE_PING = "ping" +SERVICE_REFRESH_VALUE = "refresh_value" SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters" +ATTR_NODES = "nodes" +# config parameter ATTR_CONFIG_PARAMETER = "parameter" ATTR_CONFIG_PARAMETER_BITMASK = "bitmask" ATTR_CONFIG_VALUE = "value" - -SERVICE_REFRESH_VALUE = "refresh_value" - +# refresh value ATTR_REFRESH_ALL_VALUES = "refresh_all_values" - -SERVICE_SET_VALUE = "set_value" -SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" - +# multicast ATTR_BROADCAST = "broadcast" - -SERVICE_PING = "ping" +# meter reset +ATTR_METER_TYPE = "meter_type" ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index f20a12a519a..7c41ad035be 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import cast +import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, ConfigurationValueType from zwave_js_server.model.node import Node as ZwaveNode @@ -26,10 +27,11 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity from .helpers import get_device_id @@ -89,6 +91,16 @@ async def async_setup_entry( ) ) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RESET_METER, + { + vol.Optional(ATTR_METER_TYPE): vol.Coerce(int), + vol.Optional(ATTR_VALUE): vol.Coerce(int), + }, + "async_reset_meter", + ) + class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" @@ -218,6 +230,30 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) + async def async_reset_meter( + self, meter_type: int | None = None, value: int | None = None + ) -> None: + """Reset meter(s) on device.""" + node = self.info.node + primary_value = self.info.primary_value + if primary_value.command_class != CommandClass.METER: + raise TypeError("Reset only available for Meter sensors") + options = {} + if meter_type is not None: + options["type"] = meter_type + if value is not None: + options["targetValue"] = value + args = [options] if options else [] + await node.endpoints[primary_value.endpoint].async_invoke_cc_api( + CommandClass.METER, "reset", *args, wait_for_result=False + ) + LOGGER.debug( + "Meters on node %s endpoint %s reset with the following options: %s", + node, + primary_value.endpoint, + options, + ) + class ZWaveListSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor with multiple states.""" diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index f737f159806..b41a893c7e4 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -229,3 +229,26 @@ ping: target: entity: integration: zwave_js + +reset_meter: + name: Reset meter(s) on a node + description: Resets the meter(s) on a node. + target: + entity: + domain: sensor + integration: zwave_js + fields: + meter_type: + name: Meter Type + description: The type of meter to reset. Not all meters support the ability to pick a meter type to reset. + example: 1 + required: false + selector: + text: + value: + name: Target Value + description: The value that meter(s) should be reset to. Not all meters support the ability to be reset to a specific value. + example: 5 + required: false + selector: + text: diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index fc6d274235d..e368ec1b026 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,7 +1,14 @@ """Test the Z-Wave JS sensor platform.""" from zwave_js_server.event import Event +from homeassistant.components.zwave_js.const import ( + ATTR_METER_TYPE, + ATTR_VALUE, + DOMAIN, + SERVICE_RESET_METER, +) from homeassistant.const import ( + ATTR_ENTITY_ID, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, @@ -131,3 +138,55 @@ async def test_node_status_sensor(hass, lock_id_lock_as_id150, integration): ) node.receive_event(event) assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" + + +async def test_reset_meter( + hass, + client, + aeon_smart_switch_6, + integration, +): + """Test reset_meter service.""" + SENSOR = "sensor.smart_switch_6_electric_consumed_v" + client.async_send_command.return_value = {} + client.async_send_command_no_wait.return_value = {} + + # Test successful meter reset call + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: SENSOR, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "endpoint.invoke_cc_api" + assert args["nodeId"] == aeon_smart_switch_6.node_id + assert args["endpoint"] == 0 + assert args["args"] == [] + + client.async_send_command_no_wait.reset_mock() + + # Test successful meter reset call with options + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: SENSOR, + ATTR_METER_TYPE: 1, + ATTR_VALUE: 2, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "endpoint.invoke_cc_api" + assert args["nodeId"] == aeon_smart_switch_6.node_id + assert args["endpoint"] == 0 + assert args["args"] == [{"type": 1, "targetValue": 2}] + + client.async_send_command_no_wait.reset_mock() From f13d7f189ade8f241f761ff21bf0e010e732b8ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 18:09:49 -0500 Subject: [PATCH 714/818] Fix invalid homekit state when arming (#53646) - Maybe fixes #48538 --- .../homekit/type_security_systems.py | 141 +++++++++--------- .../homekit/test_type_security_systems.py | 63 +++++++- 2 files changed, 127 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index acbf636c1c3..6fe1a4e9e29 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -2,13 +2,13 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM -from pyhap.loader import get_loader from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_TRIGGER, ) from homeassistant.const import ( @@ -22,6 +22,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -36,28 +38,43 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = { - STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, - STATE_ALARM_ARMED_NIGHT: 2, - STATE_ALARM_DISARMED: 3, - STATE_ALARM_TRIGGERED: 4, +HK_ALARM_STAY_ARMED = 0 +HK_ALARM_AWAY_ARMED = 1 +HK_ALARM_NIGHT_ARMED = 2 +HK_ALARM_DISARMED = 3 +HK_ALARM_TRIGGERED = 4 + +HASS_TO_HOMEKIT_CURRENT = { + STATE_ALARM_ARMED_HOME: HK_ALARM_STAY_ARMED, + STATE_ALARM_ARMED_VACATION: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_AWAY: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_NIGHT: HK_ALARM_NIGHT_ARMED, + STATE_ALARM_ARMING: HK_ALARM_DISARMED, + STATE_ALARM_DISARMED: HK_ALARM_DISARMED, + STATE_ALARM_TRIGGERED: HK_ALARM_TRIGGERED, +} + +HASS_TO_HOMEKIT_TARGET = { + STATE_ALARM_ARMED_HOME: HK_ALARM_STAY_ARMED, + STATE_ALARM_ARMED_VACATION: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_AWAY: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_NIGHT: HK_ALARM_NIGHT_ARMED, + STATE_ALARM_ARMING: HK_ALARM_AWAY_ARMED, + STATE_ALARM_DISARMED: HK_ALARM_DISARMED, } HASS_TO_HOMEKIT_SERVICES = { - SERVICE_ALARM_ARM_HOME: 0, - SERVICE_ALARM_ARM_AWAY: 1, - SERVICE_ALARM_ARM_NIGHT: 2, - SERVICE_ALARM_DISARM: 3, + SERVICE_ALARM_ARM_HOME: HK_ALARM_STAY_ARMED, + SERVICE_ALARM_ARM_AWAY: HK_ALARM_AWAY_ARMED, + SERVICE_ALARM_ARM_NIGHT: HK_ALARM_NIGHT_ARMED, + SERVICE_ALARM_DISARM: HK_ALARM_DISARMED, } -HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} - -STATE_TO_SERVICE = { - STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, - STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, - STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, - STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM, +HK_TO_SERVICE = { + HK_ALARM_AWAY_ARMED: SERVICE_ALARM_ARM_AWAY, + HK_ALARM_STAY_ARMED: SERVICE_ALARM_ARM_HOME, + HK_ALARM_NIGHT_ARMED: SERVICE_ALARM_ARM_NIGHT, + HK_ALARM_DISARMED: SERVICE_ALARM_DISARM, } @@ -75,65 +92,51 @@ class SecuritySystem(HomeAccessory): ATTR_SUPPORTED_FEATURES, ( SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_VACATION | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_TRIGGER ), ) - loader = get_loader() - default_current_states = loader.get_char( - "SecuritySystemCurrentState" - ).properties.get("ValidValues") - default_target_services = loader.get_char( - "SecuritySystemTargetState" - ).properties.get("ValidValues") + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) + current_char = serv_alarm.get_characteristic(CHAR_CURRENT_SECURITY_STATE) + target_char = serv_alarm.get_characteristic(CHAR_TARGET_SECURITY_STATE) + default_current_states = current_char.properties.get("ValidValues") + default_target_services = target_char.properties.get("ValidValues") - current_supported_states = [ - HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], - HASS_TO_HOMEKIT[STATE_ALARM_TRIGGERED], - ] - target_supported_services = [HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM]] + current_supported_states = [HK_ALARM_DISARMED, HK_ALARM_TRIGGERED] + target_supported_services = [HK_ALARM_DISARMED] if supported_states & SUPPORT_ALARM_ARM_HOME: - current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_HOME]) - target_supported_services.append( - HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_HOME] - ) + current_supported_states.append(HK_ALARM_STAY_ARMED) + target_supported_services.append(HK_ALARM_STAY_ARMED) - if supported_states & SUPPORT_ALARM_ARM_AWAY: - current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_AWAY]) - target_supported_services.append( - HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_AWAY] - ) + if supported_states & (SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_VACATION): + current_supported_states.append(HK_ALARM_AWAY_ARMED) + target_supported_services.append(HK_ALARM_AWAY_ARMED) if supported_states & SUPPORT_ALARM_ARM_NIGHT: - current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_NIGHT]) - target_supported_services.append( - HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_NIGHT] - ) + current_supported_states.append(HK_ALARM_NIGHT_ARMED) + target_supported_services.append(HK_ALARM_NIGHT_ARMED) - new_current_states = { - key: val - for key, val in default_current_states.items() - if val in current_supported_states - } - new_target_services = { - key: val - for key, val in default_target_services.items() - if val in target_supported_services - } - - serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) self.char_current_state = serv_alarm.configure_char( CHAR_CURRENT_SECURITY_STATE, - value=HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], - valid_values=new_current_states, + value=HASS_TO_HOMEKIT_CURRENT[STATE_ALARM_DISARMED], + valid_values={ + key: val + for key, val in default_current_states.items() + if val in current_supported_states + }, ) self.char_target_state = serv_alarm.configure_char( CHAR_TARGET_SECURITY_STATE, value=HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM], - valid_values=new_target_services, + valid_values={ + key: val + for key, val in default_target_services.items() + if val in target_supported_services + }, setter_callback=self.set_security_state, ) @@ -144,9 +147,7 @@ class SecuritySystem(HomeAccessory): def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set security state to %d", self.entity_id, value) - hass_value = HOMEKIT_TO_HASS[value] - service = STATE_TO_SERVICE[hass_value] - + service = HK_TO_SERVICE[value] params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code @@ -156,20 +157,16 @@ class SecuritySystem(HomeAccessory): def async_update_state(self, new_state): """Update security state after state changed.""" hass_state = new_state.state - if hass_state in HASS_TO_HOMEKIT: - current_security_state = HASS_TO_HOMEKIT[hass_state] - if self.char_current_state.value != current_security_state: - self.char_current_state.set_value(current_security_state) + if (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None: + if self.char_current_state.value != current_state: + self.char_current_state.set_value(current_state) _LOGGER.debug( "%s: Updated current state to %s (%d)", self.entity_id, hass_state, - current_security_state, + current_state, ) - # SecuritySystemTargetState does not support triggered - if ( - hass_state != STATE_ALARM_TRIGGERED - and self.char_target_state.value != current_security_state - ): - self.char_target_state.set_value(current_security_state) + if (target_state := HASS_TO_HOMEKIT_TARGET.get(hass_state)) is not None: + if self.char_target_state.value != target_state: + self.char_target_state.set_value(target_state) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 19b8b5720e2..d1ce830a0e2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -17,6 +17,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, @@ -79,7 +81,7 @@ async def test_switch_set_state(hass, hk_driver, events): call_arm_night = async_mock_service(hass, DOMAIN, "alarm_arm_night") call_disarm = async_mock_service(hass, DOMAIN, "alarm_disarm") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert call_arm_home assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id @@ -88,7 +90,7 @@ async def test_switch_set_state(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 1) + acc.char_target_state.client_update_value(1) await hass.async_block_till_done() assert call_arm_away assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id @@ -97,7 +99,7 @@ async def test_switch_set_state(hass, hk_driver, events): assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 2) + acc.char_target_state.client_update_value(2) await hass.async_block_till_done() assert call_arm_night assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id @@ -106,7 +108,7 @@ async def test_switch_set_state(hass, hk_driver, events): assert len(events) == 3 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 3) + acc.char_target_state.client_update_value(3) await hass.async_block_till_done() assert call_disarm assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id @@ -128,7 +130,7 @@ async def test_no_alarm_code(hass, hk_driver, config, events): # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, "alarm_arm_home") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert call_arm_home assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id @@ -138,6 +140,57 @@ async def test_no_alarm_code(hass, hk_driver, config, events): assert events[-1].data[ATTR_VALUE] is None +async def test_arming(hass, hk_driver, events): + """Test to make sure arming sets the right state.""" + entity_id = "alarm_control_panel.test" + + hass.states.async_set(entity_id, None) + + acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, {}) + await acc.run() + await hass.async_block_till_done() + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert acc.char_target_state.value == 0 + assert acc.char_current_state.value == 0 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_VACATION) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert acc.char_target_state.value == 2 + assert acc.char_current_state.value == 2 + + hass.states.async_set(entity_id, STATE_ALARM_ARMING) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 3 + + hass.states.async_set(entity_id, STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 3 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 + + hass.states.async_set(entity_id, STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 4 + + async def test_supported_states(hass, hk_driver, events): """Test different supported states.""" code = "1234" From fce7417ed122eaa918e81f8311cba6e9825949ba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 17:31:10 -0700 Subject: [PATCH 715/818] Add last reset to enphase sensors (#53653) --- .../components/enphase_envoy/__init__.py | 8 +- .../components/enphase_envoy/const.py | 87 +++++++++++++++---- .../components/enphase_envoy/sensor.py | 43 ++++----- 3 files changed, 88 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index dfd6b782408..69c488169a6 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -47,9 +47,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except httpx.HTTPError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - for condition in SENSORS: - if condition != "inverters": - data[condition] = await getattr(envoy_reader, condition)() + for description in SENSORS: + if description.key != "inverters": + data[description.key] = await getattr( + envoy_reader, description.key + )() else: data[ "inverters_production" diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 7a1de25e242..9f87a821787 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,8 +1,12 @@ """The enphase_envoy component.""" -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) +from homeassistant.const import DEVICE_CLASS_ENERGY, ENERGY_WATT_HOUR, POWER_WATT +from homeassistant.util import dt DOMAIN = "enphase_envoy" @@ -12,22 +16,67 @@ PLATFORMS = ["sensor"] COORDINATOR = "coordinator" NAME = "name" -SENSORS = { - "production": ("Current Energy Production", POWER_WATT, STATE_CLASS_MEASUREMENT), - "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR, None), - "seven_days_production": ( - "Last Seven Days Energy Production", - ENERGY_WATT_HOUR, - None, +SENSORS = ( + SensorEntityDescription( + key="production", + name="Current Power Production", + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, ), - "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR, None), - "consumption": ("Current Energy Consumption", POWER_WATT, STATE_CLASS_MEASUREMENT), - "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR, None), - "seven_days_consumption": ( - "Last Seven Days Energy Consumption", - ENERGY_WATT_HOUR, - None, + SensorEntityDescription( + key="daily_production", + name="Today's Energy Production", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, ), - "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR, None), - "inverters": ("Inverter", POWER_WATT, STATE_CLASS_MEASUREMENT), -} + SensorEntityDescription( + key="seven_days_production", + name="Last Seven Days Energy Production", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + ), + SensorEntityDescription( + key="lifetime_production", + name="Lifetime Energy Production", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + ), + SensorEntityDescription( + key="consumption", + name="Current Power Consumption", + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="daily_consumption", + name="Today's Energy Consumption", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + ), + SensorEntityDescription( + key="seven_days_consumption", + name="Last Seven Days Energy Consumption", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + ), + SensorEntityDescription( + key="lifetime_consumption", + name="Lifetime Energy Consumption", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + ), + SensorEntityDescription( + key="inverters", + name="Inverter", + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), +) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 3fab9e320dc..29d273401f4 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -56,42 +56,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = data[NAME] entities = [] - for condition, sensor in SENSORS.items(): + for sensor_description in SENSORS: if ( - condition == "inverters" + sensor_description.key == "inverters" and coordinator.data.get("inverters_production") is not None ): for inverter in coordinator.data["inverters_production"]: - entity_name = f"{name} {sensor[0]} {inverter}" + entity_name = f"{name} {sensor_description.name} {inverter}" split_name = entity_name.split(" ") serial_number = split_name[-1] entities.append( Envoy( - condition, + sensor_description, entity_name, name, config_entry.unique_id, serial_number, - sensor[1], - sensor[2], coordinator, ) ) - elif condition != "inverters": - data = coordinator.data.get(condition) + elif sensor_description.key != "inverters": + data = coordinator.data.get(sensor_description.key) if isinstance(data, str) and "not available" in data: continue - entity_name = f"{name} {sensor[0]}" + entity_name = f"{name} {sensor_description.name}" entities.append( Envoy( - condition, + sensor_description, entity_name, name, config_entry.unique_id, None, - sensor[1], - sensor[2], coordinator, ) ) @@ -104,23 +100,19 @@ class Envoy(CoordinatorEntity, SensorEntity): def __init__( self, - sensor_type, + description, name, device_name, device_serial_number, serial_number, - unit, - state_class, coordinator, ): """Initialize Envoy entity.""" - self._type = sensor_type + self.entity_description = description self._name = name self._serial_number = serial_number self._device_name = device_name self._device_serial_number = device_serial_number - self._unit_of_measurement = unit - self._attr_state_class = state_class super().__init__(coordinator) @@ -135,16 +127,16 @@ class Envoy(CoordinatorEntity, SensorEntity): if self._serial_number: return self._serial_number if self._device_serial_number: - return f"{self._device_serial_number}_{self._type}" + return f"{self._device_serial_number}_{self.entity_description.key}" @property def state(self): """Return the state of the sensor.""" - if self._type != "inverters": - value = self.coordinator.data.get(self._type) + if self.entity_description.key != "inverters": + value = self.coordinator.data.get(self.entity_description.key) elif ( - self._type == "inverters" + self.entity_description.key == "inverters" and self.coordinator.data.get("inverters_production") is not None ): value = self.coordinator.data.get("inverters_production").get( @@ -155,11 +147,6 @@ class Envoy(CoordinatorEntity, SensorEntity): return value - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - @property def icon(self): """Icon to use in the frontend, if any.""" @@ -169,7 +156,7 @@ class Envoy(CoordinatorEntity, SensorEntity): def extra_state_attributes(self): """Return the state attributes.""" if ( - self._type == "inverters" + self.entity_description.key == "inverters" and self.coordinator.data.get("inverters_production") is not None ): value = self.coordinator.data.get("inverters_production").get( From c9d355a8a4abe5387fee300a3d05e8c1953921bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 15:48:27 -0700 Subject: [PATCH 716/818] Add last reset to Shelly (#53654) --- homeassistant/components/shelly/entity.py | 2 ++ homeassistant/components/shelly/sensor.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 743dd07414e..c8b23f71bd7 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass +from datetime import datetime import logging from typing import Any, Callable, Final, cast @@ -179,6 +180,7 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None + last_reset: datetime | None = None @dataclass diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 96ff6e55f8d..7c2ffdbc470 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,6 +1,7 @@ """Sensor for Shelly.""" from __future__ import annotations +from datetime import datetime from typing import Final, cast from homeassistant.components import sensor @@ -20,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import dt from .const import SHAIR_MAX_WORK_HOURS from .entity import ( @@ -119,6 +121,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", @@ -126,6 +129,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), ), ("light", "energy"): BlockAttributeDescription( name="Energy", @@ -257,6 +261,11 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """State class of sensor.""" return self.description.state_class + @property + def last_reset(self) -> datetime | None: + """State class of sensor.""" + return self.description.last_reset + @property def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" From 5483300668ca14138f73cf77af9e28c6d92b0e20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 19:39:45 -0500 Subject: [PATCH 717/818] Bump aiolip to 1.1.6 to fix timeout with ident (#53660) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index e0acf31e99c..b6f0785ffe7 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.11.0", "aiolip==1.1.4"], + "requirements": ["pylutron-caseta==0.11.0", "aiolip==1.1.6"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index bfa61df2f9e..6f9381d19ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,7 +203,7 @@ aiolifx==0.6.9 aiolifx_effects==0.2.2 # homeassistant.components.lutron_caseta -aiolip==1.1.4 +aiolip==1.1.6 # homeassistant.components.lyric aiolyric==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d2e39327df..b1f1ba01960 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ aiohue==2.5.1 aiokafka==0.6.0 # homeassistant.components.lutron_caseta -aiolip==1.1.4 +aiolip==1.1.6 # homeassistant.components.lyric aiolyric==1.0.7 From 384ddbafab6af0bdb14915c362e2f748c527113f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 23:58:28 -0500 Subject: [PATCH 718/818] Add device class energy and last reset to sense (#53667) --- homeassistant/components/sense/sensor.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index dd522d012a5..5a352969c3b 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,7 +1,10 @@ """Support for monitoring a Sense energy sensor.""" +import datetime + from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -9,6 +12,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util from .const import ( ACTIVE_NAME, @@ -218,6 +222,8 @@ class SenseVoltageSensor(SensorEntity): class SenseTrendsSensor(SensorEntity): """Implementation of a Sense energy sensor.""" + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_state_class = STATE_CLASS_MEASUREMENT _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON @@ -252,6 +258,13 @@ class SenseTrendsSensor(SensorEntity): """Return if entity is available.""" return self._had_any_update and self._coordinator.last_update_success + @property + def last_reset(self) -> datetime.datetime: + """Return the time when the sensor was last reset, if any.""" + if self._sensor_type == "DAY": + return dt_util.start_of_local_day() + return None + @callback def _async_update(self): """Track if we had an update so we do not report zero data.""" From 75dc55418bdeb3ef976600b26e5601eb356040f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 22:06:24 -0700 Subject: [PATCH 719/818] Bumped version to 2021.8.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 69a06c04f02..15d0a3485cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2ffc779f3d620b78736a3ba4265c5dc6ae667dd8 Mon Sep 17 00:00:00 2001 From: Stephen Beechen Date: Wed, 28 Jul 2021 23:12:59 -0600 Subject: [PATCH 720/818] Allow uploading large snapshots (#53528) Co-authored-by: Pascal Vizeli --- homeassistant/components/hassio/http.py | 54 +++++++------------------ tests/components/hassio/test_http.py | 49 ++++++++++++++++------ 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 302cc00bb9f..73e5549be9a 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,16 +1,15 @@ """HTTP Support for Hass.io.""" from __future__ import annotations -import asyncio import logging import os import re import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE +from aiohttp.client import ClientError, ClientTimeout +from aiohttp.hdrs import CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway -import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded @@ -20,8 +19,6 @@ from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) -MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 - NO_TIMEOUT = re.compile( r"^(?:" r"|homeassistant/update" @@ -75,48 +72,28 @@ class HassIOView(HomeAssistantView): async def _command_proxy( self, path: str, request: web.Request - ) -> web.Response | web.StreamResponse: + ) -> web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ - read_timeout = _get_timeout(path) - client_timeout = 10 - data = None headers = _init_header(request) if path in ("snapshots/new/upload", "backups/new/upload"): # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access - - # Backups are big, so we need to adjust the allowed size - request._client_max_size = ( # pylint: disable=protected-access - MAX_UPLOAD_SIZE - ) - client_timeout = 300 - try: - with async_timeout.timeout(client_timeout): - data = await request.read() - - method = getattr(self._websession, request.method.lower()) - client = await method( - f"http://{self._host}/{path}", - data=data, + # Stream the request to the supervisor + client = await self._websession.request( + method=request.method, + url=f"http://{self._host}/{path}", headers=headers, - timeout=read_timeout, + data=request.content, + timeout=_get_timeout(path), ) - # Simple request - if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: - # Return Response - body = await client.read() - return web.Response( - content_type=client.content_type, status=client.status, body=body - ) - - # Stream response + # Stream the supervisor response back response = web.StreamResponse(status=client.status, headers=client.headers) response.content_type = client.content_type @@ -126,12 +103,9 @@ class HassIOView(HomeAssistantView): return response - except aiohttp.ClientError as err: + except ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) - except asyncio.TimeoutError: - _LOGGER.error("Client timeout error on API request %s", path) - raise HTTPBadGateway() @@ -151,11 +125,11 @@ def _init_header(request: web.Request) -> dict[str, str]: return headers -def _get_timeout(path: str) -> int: +def _get_timeout(path: str) -> ClientTimeout: """Return timeout for a URL path.""" if NO_TIMEOUT.match(path): - return 0 - return 300 + return ClientTimeout(connect=10) + return ClientTimeout(connect=10, total=300) def _need_auth(hass, path: str) -> bool: diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index fc4bb3e6a0d..881d3cc26ed 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,11 +1,13 @@ """The tests for the hassio component.""" -import asyncio -from unittest.mock import patch - +from aiohttp.client import ClientError +from aiohttp.streams import StreamReader +from aiohttp.test_utils import TestClient import pytest from homeassistant.components.hassio.http import _need_auth +from tests.test_util.aiohttp import AiohttpClientMocker + async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" @@ -106,16 +108,6 @@ async def test_forward_log_request(hassio_client, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 -async def test_bad_gateway_when_cannot_find_supervisor(hassio_client): - """Test we get a bad gateway error if we can't find supervisor.""" - with patch( - "homeassistant.components.hassio.http.async_timeout.timeout", - side_effect=asyncio.TimeoutError, - ): - resp = await hassio_client.get("/api/hassio/addons/test/info") - assert resp.status == 502 - - async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mock): """Test that we forward user info correctly.""" aioclient_mock.get("http://127.0.0.1/hello") @@ -171,6 +163,37 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): assert resp.headers["Content-Disposition"] == content_disposition +async def test_supervisor_client_error( + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker +): + """Test any client error from the supervisor returns a 502.""" + # Create a request that throws a ClientError + async def raise_client_error(*args): + raise ClientError() + + aioclient_mock.get( + "http://127.0.0.1/test/raise/error", + side_effect=raise_client_error, + ) + + # Verify it returns bad gateway + resp = await hassio_client.get("/api/hassio/test/raise/error") + assert resp.status == 502 + assert len(aioclient_mock.mock_calls) == 1 + + +async def test_streamed_requests( + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker +): + """Test requests get proxied to the supervisor as a stream.""" + aioclient_mock.get("http://127.0.0.1/test/stream") + await hassio_client.get("/api/hassio/test/stream", data="Test data") + assert len(aioclient_mock.mock_calls) == 1 + + # Verify the request body is passed as a StreamReader + assert isinstance(aioclient_mock.mock_calls[0][2], StreamReader) + + def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") From b5f0c2cef4b0533e89109e6e00535161f53d6e97 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 Jul 2021 20:02:47 +0200 Subject: [PATCH 721/818] Move TP-Link power and energy switch attributes to sensors (#53596) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tplink/__init__.py | 127 ++++++++++-- homeassistant/components/tplink/common.py | 33 +-- homeassistant/components/tplink/const.py | 76 +++++++ homeassistant/components/tplink/sensor.py | 100 +++++++++ homeassistant/components/tplink/switch.py | 201 ++++-------------- tests/components/tplink/consts.py | 72 +++++++ tests/components/tplink/test_init.py | 219 +++++++++++++++++++- tests/components/tplink/test_light.py | 2 +- 8 files changed, 634 insertions(+), 196 deletions(-) create mode 100644 homeassistant/components/tplink/sensor.py create mode 100644 tests/components/tplink/consts.py diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 1f843d364d8..9d5263687f8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -1,37 +1,57 @@ """Component to embed TP-Link smart home devices.""" -import logging +from __future__ import annotations +from datetime import datetime, timedelta +import logging +import time + +from pyHS100.smartdevice import SmartDevice, SmartDeviceException +from pyHS100.smartplug import SmartPlug import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ALIAS, + CONF_DEVICE_ID, + CONF_HOST, + CONF_MAC, + CONF_STATE, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.dt import utc_from_timestamp -from .common import ( +from .common import SmartDevices, async_discover_devices, get_static_devices +from .const import ( ATTR_CONFIG, + ATTR_CURRENT_A, + ATTR_TOTAL_ENERGY_KWH, CONF_DIMMER, CONF_DISCOVERY, + CONF_EMETER_PARAMS, CONF_LIGHT, + CONF_MODEL, CONF_STRIP, + CONF_SW_VERSION, CONF_SWITCH, - SmartDevices, - async_discover_devices, - get_static_devices, + COORDINATORS, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) DOMAIN = "tplink" -PLATFORMS = [CONF_LIGHT, CONF_SWITCH] - TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -82,8 +102,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_count = len(tplink_devices) # These will contain the initialized devices - lights = hass.data[DOMAIN][CONF_LIGHT] = [] - switches = hass.data[DOMAIN][CONF_SWITCH] = [] + hass.data[DOMAIN][CONF_LIGHT] = [] + hass.data[DOMAIN][CONF_SWITCH] = [] + lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT] + switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH] # Add static devices static_devices = SmartDevices() @@ -102,14 +124,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: lights.extend(discovered_devices.lights) switches.extend(discovered_devices.switches) - forward_setup = hass.config_entries.async_forward_entry_setup if lights: _LOGGER.debug( "Got %s lights: %s", len(lights), ", ".join(d.host for d in lights) ) - hass.async_create_task(forward_setup(entry, "light")) - if switches: _LOGGER.debug( "Got %s switches: %s", @@ -117,7 +136,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ", ".join(d.host for d in switches), ) - hass.async_create_task(forward_setup(entry, "switch")) + # prepare DataUpdateCoordinators + hass.data[DOMAIN][COORDINATORS] = {} + for switch in switches: + + try: + await hass.async_add_executor_job(switch.get_sysinfo) + except SmartDeviceException as ex: + _LOGGER.debug(ex) + raise ConfigEntryNotReady from ex + + hass.data[DOMAIN][COORDINATORS][ + switch.mac + ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) + + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -130,3 +165,65 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].clear() return unload_ok + + +class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for specific SmartPlug.""" + + def __init__( + self, + hass: HomeAssistant, + smartplug: SmartPlug, + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" + self.smartplug = smartplug + + update_interval = timedelta(seconds=30) + super().__init__( + hass, _LOGGER, name=smartplug.alias, update_interval=update_interval + ) + + async def _async_update_data(self) -> dict: + """Fetch all device and sensor data from api.""" + info = self.smartplug.sys_info + data = { + CONF_HOST: self.smartplug.host, + CONF_MAC: info["mac"], + CONF_MODEL: info["model"], + CONF_SW_VERSION: info["sw_ver"], + } + if self.smartplug.context is None: + data[CONF_ALIAS] = info["alias"] + data[CONF_DEVICE_ID] = info["mac"] + data[CONF_STATE] = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + else: + plug_from_context = next( + c + for c in self.smartplug.sys_info["children"] + if c["id"] == self.smartplug.context + ) + data[CONF_ALIAS] = plug_from_context["alias"] + data[CONF_DEVICE_ID] = self.smartplug.context + data[CONF_STATE] = plug_from_context["state"] == 1 + if self.smartplug.has_emeter: + emeter_readings = self.smartplug.get_emeter_realtime() + data[CONF_EMETER_PARAMS] = { + ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), + ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), + ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), + ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), + ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, + } + emeter_statics = self.smartplug.get_emeter_daily() + data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TODAY_ENERGY_KWH + ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + if emeter_statics.get(int(time.strftime("%e"))): + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( + float(emeter_statics[int(time.strftime("%e"))]), 3 + ) + else: + # today's consumption not available, when device was off all the day + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0 + + return data diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 84ce7edadb6..6f6fb0a14c2 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -14,21 +14,20 @@ from pyHS100 import ( ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from .const import DOMAIN as TPLINK_DOMAIN +from .const import ( + CONF_DIMMER, + CONF_LIGHT, + CONF_STRIP, + CONF_SWITCH, + DOMAIN as TPLINK_DOMAIN, + MAX_DISCOVERY_RETRIES, +) _LOGGER = logging.getLogger(__name__) -ATTR_CONFIG = "config" -CONF_DIMMER = "dimmer" -CONF_DISCOVERY = "discovery" -CONF_LIGHT = "light" -CONF_STRIP = "strip" -CONF_SWITCH = "switch" -MAX_DISCOVERY_RETRIES = 4 - - class SmartDevices: """Hold different kinds of devices.""" @@ -98,7 +97,7 @@ async def async_discover_devices( else: _LOGGER.error("Unknown smart device type: %s", type(dev)) - devices = {} + devices: dict[str, SmartDevice] = {} for attempt in range(1, MAX_DISCOVERY_RETRIES + 1): _LOGGER.debug( "Discovering tplink devices, attempt %s of %s", @@ -159,16 +158,18 @@ def get_static_devices(config_data) -> SmartDevices: def add_available_devices( hass: HomeAssistant, device_type: str, device_class: Callable -) -> list: +) -> list[Entity]: """Get sysinfo for all devices.""" - devices = hass.data[TPLINK_DOMAIN][device_type] + devices: list[SmartDevice] = hass.data[TPLINK_DOMAIN][device_type] if f"{device_type}_remaining" in hass.data[TPLINK_DOMAIN]: - devices = hass.data[TPLINK_DOMAIN][f"{device_type}_remaining"] + devices: list[SmartDevice] = hass.data[TPLINK_DOMAIN][ + f"{device_type}_remaining" + ] - entities_ready = [] - devices_unavailable = [] + entities_ready: list[Entity] = [] + devices_unavailable: list[SmartDevice] = [] for device in devices: try: device.get_sysinfo() diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 8b85b8afd74..93cad889a2f 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -1,5 +1,81 @@ """Const for TP-Link.""" +from __future__ import annotations + import datetime +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.const import ( + ATTR_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) + DOMAIN = "tplink" +COORDINATORS = "coordinators" + MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) +MAX_DISCOVERY_RETRIES = 4 + +ATTR_CONFIG = "config" +ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" +ATTR_CURRENT_A = "current_a" + +CONF_MODEL = "model" +CONF_SW_VERSION = "sw_ver" +CONF_EMETER_PARAMS = "emeter_params" +CONF_DIMMER = "dimmer" +CONF_DISCOVERY = "discovery" +CONF_LIGHT = "light" +CONF_STRIP = "strip" +CONF_SWITCH = "switch" +CONF_SENSOR = "sensor" + +PLATFORMS = [CONF_LIGHT, CONF_SENSOR, CONF_SWITCH] + +ENERGY_SENSORS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key=ATTR_CURRENT_POWER_W, + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + name="Current Consumption", + ), + SensorEntityDescription( + key=ATTR_TOTAL_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Total Consumption", + ), + SensorEntityDescription( + key=ATTR_TODAY_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Today's Consumption", + ), + SensorEntityDescription( + key=ATTR_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + name="Voltage", + ), + SensorEntityDescription( + key=ATTR_CURRENT_A, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + name="Current", + ), +] diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py new file mode 100644 index 00000000000..24b93e90963 --- /dev/null +++ b/homeassistant/components/tplink/sensor.py @@ -0,0 +1,100 @@ +"""Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" +from __future__ import annotations + +from typing import Any + +from pyHS100 import SmartPlug + +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC +from homeassistant.core import HomeAssistant +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONF_EMETER_PARAMS, + CONF_MODEL, + CONF_SW_VERSION, + CONF_SWITCH, + COORDINATORS, + DOMAIN as TPLINK_DOMAIN, + ENERGY_SENSORS, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches.""" + entities: list[SmartPlugSensor] = [] + coordinators: list[SmartPlugDataUpdateCoordinator] = hass.data[TPLINK_DOMAIN][ + COORDINATORS + ] + switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] + for switch in switches: + coordinator: SmartPlugDataUpdateCoordinator = coordinators[switch.mac] + if not switch.has_emeter and coordinator.data.get(CONF_EMETER_PARAMS) is None: + continue + for description in ENERGY_SENSORS: + if coordinator.data[CONF_EMETER_PARAMS].get(description.key) is not None: + entities.append(SmartPlugSensor(switch, coordinator, description)) + + async_add_entities(entities) + + +class SmartPlugSensor(CoordinatorEntity, SensorEntity): + """Representation of a TPLink Smart Plug energy sensor.""" + + def __init__( + self, + smartplug: SmartPlug, + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator) + self.smartplug = smartplug + self.entity_description = description + self._attr_name = f"{coordinator.data[CONF_ALIAS]} {description.name}" + self._attr_last_reset = coordinator.data[CONF_EMETER_PARAMS][ + ATTR_LAST_RESET + ].get(description.key) + + @property + def data(self) -> dict[str, Any]: + """Return data from DataUpdateCoordinator.""" + return self.coordinator.data + + @property + def state(self) -> float | None: + """Return the sensors state.""" + return self.data[CONF_EMETER_PARAMS][self.entity_description.key] + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"{self.data[CONF_DEVICE_ID]}_{self.entity_description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return { + "name": self.data[CONF_ALIAS], + "model": self.data[CONF_MODEL], + "manufacturer": "TP-Link", + "connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])}, + "sw_version": self.data[CONF_SW_VERSION], + } diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index d088584c4ad..688091991c3 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,40 +1,30 @@ """Support for TPLink HS100/HS110/HS200 smart switch.""" from __future__ import annotations -import asyncio -from collections.abc import Mapping -from contextlib import suppress -import logging -import time from typing import Any -from pyHS100 import SmartDeviceException, SmartPlug +from pyHS100 import SmartPlug -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - SwitchEntity, -) +from homeassistant.components.switch import SwitchEntity +from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_VOLTAGE +from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC, CONF_STATE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN -from .common import add_available_devices - -PARALLEL_UPDATES = 0 - -_LOGGER = logging.getLogger(__name__) - -ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" -ATTR_CURRENT_A = "current_a" - -MAX_ATTEMPTS = 300 -SLEEP_TIME = 2 +from .const import ( + CONF_MODEL, + CONF_SW_VERSION, + CONF_SWITCH, + COORDINATORS, + DOMAIN as TPLINK_DOMAIN, +) async def async_setup_entry( @@ -43,164 +33,65 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - entities = await hass.async_add_executor_job( - add_available_devices, hass, CONF_SWITCH, SmartPlugSwitch - ) + entities: list[SmartPlugSwitch] = [] + coordinators: list[SmartPlugDataUpdateCoordinator] = hass.data[TPLINK_DOMAIN][ + COORDINATORS + ] + switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] + for switch in switches: + coordinator = coordinators[switch.mac] + entities.append(SmartPlugSwitch(switch, coordinator)) - if entities: - async_add_entities(entities, update_before_add=True) - - if hass.data[TPLINK_DOMAIN][f"{CONF_SWITCH}_remaining"]: - raise PlatformNotReady + async_add_entities(entities) -class SmartPlugSwitch(SwitchEntity): +class SmartPlugSwitch(CoordinatorEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" - def __init__(self, smartplug: SmartPlug) -> None: + def __init__( + self, smartplug: SmartPlug, coordinator: DataUpdateCoordinator + ) -> None: """Initialize the switch.""" + super().__init__(coordinator) self.smartplug = smartplug - self._sysinfo = None - self._state = None - self._is_available = False - # Set up emeter cache - self._emeter_params = {} - self._mac = None - self._alias = None - self._model = None - self._device_id = None - self._host = None + @property + def data(self) -> dict[str, Any]: + """Return data from DataUpdateCoordinator.""" + return self.coordinator.data @property def unique_id(self) -> str | None: """Return a unique ID.""" - return self._device_id + return self.data[CONF_DEVICE_ID] @property def name(self) -> str | None: """Return the name of the Smart Plug.""" - return self._alias + return self.data[CONF_ALIAS] @property def device_info(self) -> DeviceInfo: """Return information about the device.""" return { - "name": self._alias, - "model": self._model, + "name": self.data[CONF_ALIAS], + "model": self.data[CONF_MODEL], "manufacturer": "TP-Link", - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, - "sw_version": self._sysinfo["sw_ver"], + "connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])}, + "sw_version": self.data[CONF_SW_VERSION], } - @property - def available(self) -> bool: - """Return if switch is available.""" - return self._is_available - @property def is_on(self) -> bool | None: """Return true if switch is on.""" - return self._state + return self.data[CONF_STATE] - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - self.smartplug.turn_on() + await self.hass.async_add_job(self.smartplug.turn_on) + await self.coordinator.async_refresh() - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - self.smartplug.turn_off() - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes of the device.""" - return self._emeter_params - - @property - def _plug_from_context(self) -> Any: - """Return the plug from the context.""" - children = self.smartplug.sys_info["children"] - return next(c for c in children if c["id"] == self.smartplug.context) - - def update_state(self) -> None: - """Update the TP-Link switch's state.""" - if self.smartplug.context is None: - self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON - else: - self._state = self._plug_from_context["state"] == 1 - - def attempt_update(self, update_attempt: int) -> bool: - """Attempt to get details from the TP-Link switch.""" - try: - if not self._sysinfo: - self._sysinfo = self.smartplug.sys_info - self._mac = self._sysinfo["mac"] - self._model = self._sysinfo["model"] - self._host = self.smartplug.host - if self.smartplug.context is None: - self._alias = self._sysinfo["alias"] - self._device_id = self._mac - else: - self._alias = self._plug_from_context["alias"] - self._device_id = self.smartplug.context - - self.update_state() - - if self.smartplug.has_emeter: - emeter_readings = self.smartplug.get_emeter_realtime() - - self._emeter_params[ATTR_CURRENT_POWER_W] = round( - float(emeter_readings["power"]), 2 - ) - self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = round( - float(emeter_readings["total"]), 3 - ) - self._emeter_params[ATTR_VOLTAGE] = round( - float(emeter_readings["voltage"]), 1 - ) - self._emeter_params[ATTR_CURRENT_A] = round( - float(emeter_readings["current"]), 2 - ) - - emeter_statics = self.smartplug.get_emeter_daily() - with suppress(KeyError): # Device returned no daily history - self._emeter_params[ATTR_TODAY_ENERGY_KWH] = round( - float(emeter_statics[int(time.strftime("%e"))]), 3 - ) - return True - except (SmartDeviceException, OSError) as ex: - if update_attempt == 0: - _LOGGER.debug( - "Retrying in %s seconds for %s|%s due to: %s", - SLEEP_TIME, - self._host, - self._alias, - ex, - ) - return False - - async def async_update(self) -> None: - """Update the TP-Link switch's state.""" - for update_attempt in range(MAX_ATTEMPTS): - is_ready = await self.hass.async_add_executor_job( - self.attempt_update, update_attempt - ) - - if is_ready: - self._is_available = True - if update_attempt > 0: - _LOGGER.debug( - "Device %s|%s responded after %s attempts", - self._host, - self._alias, - update_attempt, - ) - break - await asyncio.sleep(SLEEP_TIME) - - else: - if self._is_available: - _LOGGER.warning( - "Could not read state for %s|%s", self.smartplug.host, self._alias - ) - self._is_available = False + await self.hass.async_add_job(self.smartplug.turn_off) + await self.coordinator.async_refresh() diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py new file mode 100644 index 00000000000..de134ddbe07 --- /dev/null +++ b/tests/components/tplink/consts.py @@ -0,0 +1,72 @@ +"""Constants for the TP-Link component tests.""" + +SMARTPLUGSWITCH_DATA = { + "sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS110(EU)", + "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", + "oemId": "40F54B43071E9436B6395611E9D91CEA", + "hwId": "A6C77E4FDD238B53D824AC8DA361F043", + "rssi": -24, + "longitude_i": 130793, + "latitude_i": 480582, + "alias": "SmartPlug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM:ENE", + "mac": "69:F2:3C:8E:E3:47", + "updating": 0, + "led_off": 0, + "relay_state": 0, + "on_time": 0, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", + "next_action": {"type": -1}, + "err_code": 0, + }, + "realtime": { + "voltage_mv": 233957, + "current_ma": 21, + "power_mw": 0, + "total_wh": 1793, + "err_code": 0, + }, +} +SMARTSTRIPWITCH_DATA = { + "sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS110(EU)", + "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", + "oemId": "40F54B43071E9436B6395611E9D91CEA", + "hwId": "A6C77E4FDD238B53D824AC8DA361F043", + "rssi": -24, + "longitude_i": 130793, + "latitude_i": 480582, + "alias": "SmartPlug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM", + "mac": "69:F2:3C:8E:E3:47", + "updating": 0, + "led_off": 0, + "relay_state": 0, + "on_time": 0, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", + "next_action": {"type": -1}, + "children": [{"id": "1", "state": 1, "alias": "SmartPlug#1"}], + "err_code": 0, + }, + "realtime": { + "voltage_mv": 233957, + "current_ma": 21, + "power_mw": 0, + "total_wh": 1793, + "err_code": 0, + }, + "context": "1", +} diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 49309a6ecef..0cfb4d3d233 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,24 +1,45 @@ """Tests for the TP-Link component.""" from __future__ import annotations +from datetime import datetime +import time from typing import Any from unittest.mock import MagicMock, patch -from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug +from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug, smartstrip +from pyHS100.smartdevice import EmeterStatus import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink -from homeassistant.components.tplink.common import ( +from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.components.tplink.common import SmartDevices +from homeassistant.components.tplink.const import ( + ATTR_CURRENT_A, + ATTR_TOTAL_ENERGY_KWH, CONF_DIMMER, CONF_DISCOVERY, + CONF_EMETER_PARAMS, CONF_LIGHT, + CONF_MODEL, + CONF_SW_VERSION, CONF_SWITCH, + COORDINATORS, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ALIAS, + CONF_DEVICE_ID, + CONF_HOST, + CONF_MAC, +) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utc_from_timestamp from tests.common import MockConfigEntry, mock_coro +from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA async def test_creating_entry_tries_discover(hass): @@ -186,7 +207,7 @@ async def test_configuring_discovery_disabled(hass): assert mock_setup.call_count == 1 -async def test_platforms_are_initialized(hass): +async def test_platforms_are_initialized(hass: HomeAssistant): """Test that platforms are initialized per configuration array.""" config = { tplink.DOMAIN: { @@ -199,6 +220,8 @@ async def test_platforms_are_initialized(hass): with patch( "homeassistant.components.tplink.common.Discover.discover" ) as discover, patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", @@ -209,13 +232,141 @@ async def test_platforms_are_initialized(hass): ) as switch_setup, patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): + + light = SmartBulb("123.123.123.123") + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_emeter_realtime = MagicMock( + return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + ) + switch.get_emeter_daily = MagicMock( + return_value={int(time.strftime("%e")): 1.123} + ) + get_static_devices.return_value = SmartDevices([light], [switch]) + # patching is_dimmable is necessray to avoid misdetection as light. await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert discover.call_count == 0 - assert light_setup.call_count == 1 - assert switch_setup.call_count == 1 + assert hass.data.get(tplink.DOMAIN) + assert hass.data[tplink.DOMAIN].get(COORDINATORS) + assert hass.data[tplink.DOMAIN][COORDINATORS].get(switch.mac) + assert isinstance( + hass.data[tplink.DOMAIN][COORDINATORS][switch.mac], + tplink.SmartPlugDataUpdateCoordinator, + ) + data = hass.data[tplink.DOMAIN][COORDINATORS][switch.mac].data + assert data[CONF_HOST] == switch.host + assert data[CONF_MAC] == switch.sys_info["mac"] + assert data[CONF_MODEL] == switch.sys_info["model"] + assert data[CONF_SW_VERSION] == switch.sys_info["sw_ver"] + assert data[CONF_ALIAS] == switch.sys_info["alias"] + assert data[CONF_DEVICE_ID] == switch.sys_info["mac"] + + emeter_readings = switch.get_emeter_realtime() + assert data[CONF_EMETER_PARAMS][ATTR_VOLTAGE] == round( + float(emeter_readings["voltage"]), 1 + ) + assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_A] == round( + float(emeter_readings["current"]), 2 + ) + assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_POWER_W] == round( + float(emeter_readings["power"]), 2 + ) + assert data[CONF_EMETER_PARAMS][ATTR_TOTAL_ENERGY_KWH] == round( + float(emeter_readings["total"]), 3 + ) + assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TOTAL_ENERGY_KWH + ] == utc_from_timestamp(0) + + assert data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] == 1.123 + assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TODAY_ENERGY_KWH + ] == datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + + assert discover.call_count == 0 + assert get_static_devices.call_count == 1 + assert light_setup.call_count == 1 + assert switch_setup.call_count == 1 + + +async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): + """Test that platforms are initialized per configuration array.""" + config = { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}], + } + } + + with patch("homeassistant.components.tplink.common.Discover.discover"), patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.switch.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.sensor.SmartPlugSensor.__init__" + ) as SmartPlugSensor, patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False + ): + + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + get_static_devices.return_value = SmartDevices([], [switch]) + + await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert SmartPlugSensor.call_count == 0 + + +async def test_smartstrip_device(hass: HomeAssistant): + """Test discover a SmartStrip devices.""" + config = { + tplink.DOMAIN: { + CONF_DISCOVERY: True, + } + } + + class SmartStrip(smartstrip.SmartStrip): + """Moked SmartStrip class.""" + + def get_sysinfo(self): + return SMARTSTRIPWITCH_DATA["sysinfo"] + + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.common.SmartPlug.get_sysinfo", + return_value=SMARTSTRIPWITCH_DATA["sysinfo"], + ), patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + ): + + strip = SmartStrip("123.123.123.123") + discover.return_value = {"123.123.123.123": strip} + + assert await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert hass.data.get(tplink.DOMAIN) + assert hass.data[tplink.DOMAIN].get(COORDINATORS) + assert hass.data[tplink.DOMAIN][COORDINATORS].get(strip.mac) + assert isinstance( + hass.data[tplink.DOMAIN][COORDINATORS][strip.mac], + tplink.SmartPlugDataUpdateCoordinator, + ) + data = hass.data[tplink.DOMAIN][COORDINATORS][strip.mac].data + assert data[CONF_ALIAS] == strip.sys_info["children"][0]["alias"] + assert data[CONF_DEVICE_ID] == "1" async def test_no_config_creates_no_entry(hass): @@ -230,6 +381,42 @@ async def test_no_config_creates_no_entry(hass): assert mock_setup.call_count == 0 +async def test_not_ready(hass: HomeAssistant): + """Test for not ready when configured devices are not available.""" + config = { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}], + } + } + + with patch("homeassistant.components.tplink.common.Discover.discover"), patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.switch.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False + ): + + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) + get_static_devices.return_value = SmartDevices([], [switch]) + + await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(tplink.DOMAIN) + + assert len(entries) == 1 + assert entries[0].state is config_entries.ConfigEntryState.SETUP_RETRY + + @pytest.mark.parametrize("platform", ["switch", "light"]) async def test_unload(hass, platform): """Test that the async_unload_entry works.""" @@ -238,21 +425,35 @@ async def test_unload(hass, platform): entry.add_to_hass(hass) with patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( f"homeassistant.components.tplink.{platform}.async_setup_entry", return_value=mock_coro(True), - ) as light_setup: + ) as async_setup_entry: config = { tplink.DOMAIN: { platform: [{CONF_HOST: "123.123.123.123"}], CONF_DISCOVERY: False, } } + + light = SmartBulb("123.123.123.123") + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_emeter_realtime = MagicMock( + return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + ) + if platform == "light": + get_static_devices.return_value = SmartDevices([light], []) + elif platform == "switch": + get_static_devices.return_value = SmartDevices([], [switch]) + assert await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert len(light_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 1 assert tplink.DOMAIN in hass.data assert await tplink.async_unload_entry(hass, entry) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index ea8809bc679..c9b07529ea4 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.tplink.common import ( +from homeassistant.components.tplink.const import ( CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, From 7e6856ace832c6d2ca49ad499a715d1db415c344 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Jul 2021 03:30:50 -0400 Subject: [PATCH 722/818] Add enabled attribute to zwave_js discovery model (#53645) * Add attribute to zwave_js discovery model * Fix binary sensor entity enabled logic * Add tests --- .../components/zwave_js/binary_sensor.py | 6 -- .../components/zwave_js/discovery.py | 55 +++++++++++++++++-- homeassistant/components/zwave_js/entity.py | 3 + homeassistant/components/zwave_js/sensor.py | 9 --- tests/components/zwave_js/common.py | 2 + tests/components/zwave_js/test_sensor.py | 24 ++++++++ 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 71de7270d9a..9d72a804ca0 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -277,12 +277,6 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): if self.info.primary_value.command_class == CommandClass.BATTERY else None ) - # Legacy binary sensors are phased out (replaced by notification sensors) - # Disable by default to not confuse users - self._attr_entity_registry_enabled_default = bool( - self.info.primary_value.command_class != CommandClass.SENSOR_BINARY - or self.info.node.device_class.generic.key == 0x20 - ) @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 403ea5c9746..7c29c89dfab 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -67,6 +67,8 @@ class ZwaveDiscoveryInfo: platform_data: dict[str, Any] | None = None # additional values that need to be watched by entity additional_value_ids_to_watch: set[str] | None = None + # bool to specify whether entity should be enabled by default + entity_registry_enabled_default: bool = True @dataclass @@ -135,6 +137,8 @@ class ZWaveDiscoverySchema: allow_multi: bool = False # [optional] bool to specify whether state is assumed and events should be fired on value update assumed_state: bool = False + # [optional] bool to specify whether entity should be enabled by default + entity_registry_enabled_default: bool = True def get_config_parameter_discovery_schema( @@ -161,6 +165,7 @@ def get_config_parameter_discovery_schema( property_key_name=property_key_name, type={"number"}, ), + entity_registry_enabled_default=False, **kwargs, ) @@ -428,12 +433,33 @@ DISCOVERY_SCHEMAS = [ ], ), # binary sensors + # When CC is Sensor Binary and device class generic is Binary Sensor, entity should + # be enabled by default + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="boolean", + device_class_generic={"Binary Sensor"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SENSOR_BINARY}, + type={"boolean"}, + ), + ), + # Legacy binary sensors are phased out (replaced by notification sensors) + # Disable by default to not confuse users + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="boolean", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SENSOR_BINARY}, + type={"boolean"}, + ), + entity_registry_enabled_default=False, + ), ZWaveDiscoverySchema( platform="binary_sensor", hint="boolean", primary_value=ZWaveValueDiscoverySchema( command_class={ - CommandClass.SENSOR_BINARY, CommandClass.BATTERY, CommandClass.SENSOR_ALARM, }, @@ -456,13 +482,19 @@ DISCOVERY_SCHEMAS = [ platform="sensor", hint="string_sensor", primary_value=ZWaveValueDiscoverySchema( - command_class={ - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - }, + command_class={CommandClass.SENSOR_ALARM}, type={"string"}, ), ), + ZWaveDiscoverySchema( + platform="sensor", + hint="string_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.INDICATOR}, + type={"string"}, + ), + entity_registry_enabled_default=False, + ), # generic numeric sensors ZWaveDiscoverySchema( platform="sensor", @@ -471,12 +503,20 @@ DISCOVERY_SCHEMAS = [ command_class={ CommandClass.SENSOR_MULTILEVEL, CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, CommandClass.BATTERY, }, type={"number"}, ), ), + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.INDICATOR}, + type={"number"}, + ), + entity_registry_enabled_default=False, + ), # numeric sensors for Meter CC ZWaveDiscoverySchema( platform="sensor", @@ -500,6 +540,7 @@ DISCOVERY_SCHEMAS = [ type={"number"}, ), allow_multi=True, + entity_registry_enabled_default=False, ), # sensor for basic CC ZWaveDiscoverySchema( @@ -512,6 +553,7 @@ DISCOVERY_SCHEMAS = [ type={"number"}, property={"currentValue"}, ), + entity_registry_enabled_default=False, ), # binary switches ZWaveDiscoverySchema( @@ -697,6 +739,7 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None platform_data_template=schema.data_template, platform_data=resolved_data, additional_value_ids_to_watch=additional_value_ids_to_watch, + entity_registry_enabled_default=schema.entity_registry_enabled_default, ) if not schema.allow_multi: diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 6df7c8d546b..793eaa435d5 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -46,6 +46,9 @@ class ZWaveBaseEntity(Entity): self._attr_unique_id = get_unique_id( self.client.driver.controller.home_id, self.info.primary_value.value_id ) + self._attr_entity_registry_enabled_default = ( + self.info.entity_registry_enabled_default + ) self._attr_assumed_state = self.info.assumed_state # device is precreated in main handler self._attr_device_info = { diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 7c41ad035be..209a5b6d4aa 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -118,15 +118,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): self._attr_name = self.generate_name(include_value_name=True) self._attr_device_class = self._get_device_class() self._attr_state_class = self._get_state_class() - self._attr_entity_registry_enabled_default = True - # We hide some of the more advanced sensors by default to not overwhelm users - if self.info.primary_value.command_class in [ - CommandClass.BASIC, - CommandClass.CONFIGURATION, - CommandClass.INDICATOR, - CommandClass.NOTIFICATION, - ]: - self._attr_entity_registry_enabled_default = False def _get_device_class(self) -> str | None: """ diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index f14842609c5..7177134aa33 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -11,6 +11,8 @@ NOTIFICATION_MOTION_BINARY_SENSOR = ( "binary_sensor.multisensor_6_home_security_motion_detection" ) NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" +INDICATOR_SENSOR = "sensor.z_wave_thermostat_indicator_value" +BASIC_SENSOR = "sensor.livingroomlight_basic" PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index e368ec1b026..aae7a1c0602 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -21,9 +21,11 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, + BASIC_SENSOR, ENERGY_SENSOR, HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, + INDICATOR_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, ) @@ -88,6 +90,28 @@ async def test_disabled_notification_sensor(hass, multisensor_6, integration): assert state.attributes["value"] == 8 +async def test_disabled_indcator_sensor( + hass, climate_radio_thermostat_ct100_plus, integration +): + """Test sensor is created from Indicator CC and is disabled.""" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(INDICATOR_SENSOR) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + +async def test_disabled_basic_sensor(hass, ge_in_wall_dimmer_switch, integration): + """Test sensor is created from Basic CC and is disabled.""" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(BASIC_SENSOR) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration): """Test config parameter sensor is created.""" ent_reg = er.async_get(hass) From b3367d8b3fbbc166faa82575b93ea5707744c71d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 29 Jul 2021 06:15:28 +0100 Subject: [PATCH 723/818] Prosegur code quality improvements (#53647) --- homeassistant/components/prosegur/__init__.py | 11 +++----- .../components/prosegur/config_flow.py | 13 ++++------ tests/components/prosegur/common.py | 2 +- .../prosegur/test_alarm_control_panel.py | 6 ++--- tests/components/prosegur/test_config_flow.py | 26 +++---------------- tests/components/prosegur/test_init.py | 2 -- 6 files changed, 15 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index 2e4a0bd0ed5..3e31a1142ce 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -3,10 +3,10 @@ import logging from pyprosegur.auth import Auth -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .const import CONF_COUNTRY, DOMAIN @@ -32,12 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except ConnectionRefusedError as error: _LOGGER.error("Configured credential are invalid, %s", error) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": entry.data["entry_id"]}, - ) - ) + raise ConfigEntryAuthFailed from error except ConnectionError as error: _LOGGER.error("Could not connect with Prosegur backend: %s", error) diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index af1ae456f12..1807561663b 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -25,11 +25,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema( async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" + session = aiohttp_client.async_get_clientsession(hass) + auth = Auth(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY]) try: - session = aiohttp_client.async_get_clientsession(hass) - auth = Auth( - session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY] - ) install = await Installation.retrieve(auth) except ConnectionRefusedError: raise InvalidAuth from ConnectionRefusedError @@ -95,15 +93,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - data = self.entry.data.copy() self.hass.config_entries.async_update_entry( self.entry, data={ - **data, + **self.entry.data, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], }, diff --git a/tests/components/prosegur/common.py b/tests/components/prosegur/common.py index 504da3ea92a..bed9d987ceb 100644 --- a/tests/components/prosegur/common.py +++ b/tests/components/prosegur/common.py @@ -8,7 +8,7 @@ from tests.common import MockConfigEntry CONTRACT = "1234abcd" -async def setup_platform(hass, platform): +async def setup_platform(hass): """Set up the Prosegur platform.""" mock_entry = MockConfigEntry( domain=PROSEGUR_DOMAIN, diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 26e0c5f94b3..9ab0c0d37de 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -48,7 +48,7 @@ def mock_status(request): async def test_entity_registry(hass, mock_auth, mock_status): """Tests that the devices are registered in the entity registry.""" - await setup_platform(hass, ALARM_DOMAIN) + await setup_platform(hass) entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get(PROSEGUR_ALARM_ENTITY) @@ -74,7 +74,7 @@ async def test_connection_error(hass, mock_auth): with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass, ALARM_DOMAIN) + await setup_platform(hass) await hass.async_block_till_done() @@ -106,7 +106,7 @@ async def test_arm(hass, mock_auth, code, alarm_service, alarm_state): install.status = code with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass, ALARM_DOMAIN) + await setup_platform(hass) await hass.services.async_call( ALARM_DOMAIN, diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 447baefed23..bece0bae621 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass): with patch( "homeassistant.components.prosegur.config_flow.Installation.retrieve", return_value=install, - ), patch( + ) as mock_retrieve, patch( "homeassistant.components.prosegur.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -50,6 +50,8 @@ async def test_form(hass): } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_retrieve.mock_calls) == 1 + async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" @@ -120,28 +122,6 @@ async def test_form_unknown_exception(hass): assert result2["errors"] == {"base": "unknown"} -async def test_form_validate_input(hass): - """Test we retrieve data from Installation.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pyprosegur.installation.Installation.retrieve", - return_value=MagicMock, - ) as mock_retrieve: - await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "country": "PT", - }, - ) - - assert len(mock_retrieve.mock_calls) == 1 - - async def test_reauth_flow(hass): """Test a reauthentication flow.""" entry = MockConfigEntry( diff --git a/tests/components/prosegur/test_init.py b/tests/components/prosegur/test_init.py index e0fe596ee13..2079d7a2b3c 100644 --- a/tests/components/prosegur/test_init.py +++ b/tests/components/prosegur/test_init.py @@ -18,7 +18,6 @@ from tests.common import MockConfigEntry async def test_setup_entry_fail_retrieve(hass, error): """Test loading the Prosegur entry.""" - hass.config.components.add(DOMAIN) config_entry = MockConfigEntry( domain=DOMAIN, data={ @@ -47,7 +46,6 @@ async def test_unload_entry(hass, aioclient_mock): json={"data": {"token": "123456789"}}, ) - hass.config.components.add(DOMAIN) config_entry = MockConfigEntry( domain=DOMAIN, data={ From 2aeecba64c3dcc329b6ef71f344e2f052f607e17 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Jul 2021 23:16:14 -0600 Subject: [PATCH 724/818] Fix unhandled exception with Guardian paired sensor coordinators (#53663) --- homeassistant/components/guardian/__init__.py | 17 +++++++++-------- .../components/guardian/binary_sensor.py | 12 ++++++------ homeassistant/components/guardian/const.py | 1 + homeassistant/components/guardian/sensor.py | 12 ++++++------ 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 9338f9a47a9..915746c5ed5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -26,6 +26,7 @@ from .const import ( CONF_UID, DATA_CLIENT, DATA_COORDINATOR, + DATA_COORDINATOR_PAIRED_SENSOR, DATA_PAIRED_SENSOR_MANAGER, DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, @@ -44,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: { DATA_CLIENT: {}, DATA_COORDINATOR: {}, + DATA_COORDINATOR_PAIRED_SENSOR: {}, DATA_PAIRED_SENSOR_MANAGER: {}, DATA_UNSUB_DISPATCHER_CONNECT: {}, }, @@ -51,9 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client( entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT] ) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = { - API_SENSOR_PAIRED_SENSOR_STATUS: {} - } + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id] = {} hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id] = [] # The valve controller's UDP-based API can't handle concurrent requests very well, @@ -113,6 +114,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR].pop(entry.entry_id) for unsub in hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id]: unsub() hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT].pop(entry.entry_id) @@ -143,8 +145,8 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + self._entry.entry_id ][uid] = GuardianDataUpdateCoordinator( self._hass, client=self._client, @@ -194,8 +196,8 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + self._entry.entry_id ].pop(uid) # Remove the paired sensor device from the device registry (which will @@ -297,7 +299,6 @@ class ValveControllerEntity(GuardianEntity): return any( coordinator.last_update_success for coordinator in self.coordinators.values() - if coordinator ) async def _async_continue_entity_setup(self) -> None: diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 8ce381e0456..1cbc9f5cede 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -15,11 +15,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PairedSensorEntity, ValveControllerEntity from .const import ( - API_SENSOR_PAIRED_SENSOR_STATUS, API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, CONF_UID, DATA_COORDINATOR, + DATA_COORDINATOR_PAIRED_SENSOR, DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -49,9 +49,9 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS - ][uid] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + uid + ] entities = [] for kind in PAIRED_SENSOR_SENSORS: @@ -95,8 +95,8 @@ async def async_setup_entry( ) # Add all paired sensor-specific binary sensors: - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + entry.entry_id ].values(): for kind in PAIRED_SENSOR_SENSORS: name, device_class = SENSOR_ATTRS_MAP[kind] diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 750a8c407ca..e27e8a37047 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -16,6 +16,7 @@ CONF_UID = "uid" DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" +DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" DATA_UNSUB_DISPATCHER_CONNECT = "unsub_dispatcher_connect" diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index ce09bb99c60..2d7cde86cca 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -17,11 +17,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PairedSensorEntity, ValveControllerEntity from .const import ( - API_SENSOR_PAIRED_SENSOR_STATUS, API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, CONF_UID, DATA_COORDINATOR, + DATA_COORDINATOR_PAIRED_SENSOR, DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -54,9 +54,9 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS - ][uid] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + uid + ] entities = [] for kind in PAIRED_SENSOR_SENSORS: @@ -96,8 +96,8 @@ async def async_setup_entry( ) # Add all paired sensor-specific binary sensors: - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + entry.entry_id ].values(): for kind in PAIRED_SENSOR_SENSORS: name, device_class, icon, unit = SENSOR_ATTRS_MAP[kind] From d19d487b211d50a4fee5025c13774edb71c56009 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Jul 2021 08:18:38 -0400 Subject: [PATCH 725/818] Add energy support for zwave_js meter CC entities (#53665) * Add energy support for zwave_js meter CC entities * shrink * comments * comments * comments * Move attributes * Add tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare --- .../components/zwave_js/discovery.py | 4 +- homeassistant/components/zwave_js/sensor.py | 85 +++++++++++++++++-- tests/components/zwave_js/common.py | 6 ++ tests/components/zwave_js/conftest.py | 18 ++++ tests/components/zwave_js/test_sensor.py | 52 +++++++++--- 5 files changed, 146 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 7c29c89dfab..588b4c76472 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -517,10 +517,10 @@ DISCOVERY_SCHEMAS = [ ), entity_registry_enabled_default=False, ), - # numeric sensors for Meter CC + # Meter sensors for Meter CC ZWaveDiscoverySchema( platform="sensor", - hint="numeric_sensor", + hint="meter", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.METER, diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 209a5b6d4aa..39aa2f30604 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -11,6 +11,7 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ConfigurationValue from homeassistant.components.sensor import ( + ATTR_LAST_RESET, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, @@ -28,8 +29,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER from .discovery import ZwaveDiscoveryInfo @@ -60,6 +66,8 @@ async def async_setup_entry( entities.append(ZWaveListSensor(config_entry, client, info)) elif info.platform_hint == "config_parameter": entities.append(ZWaveConfigParameterSensor(config_entry, client, info)) + elif info.platform_hint == "meter": + entities.append(ZWaveMeterSensor(config_entry, client, info)) else: LOGGER.warning( "Sensor not implemented for %s/%s", @@ -128,10 +136,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): """ if self.info.primary_value.command_class == CommandClass.BATTERY: return DEVICE_CLASS_BATTERY - if self.info.primary_value.command_class == CommandClass.METER: - if self.info.primary_value.metadata.unit == "kWh": - return DEVICE_CLASS_ENERGY - return DEVICE_CLASS_POWER if isinstance(self.info.primary_value.property_, str): property_lower = self.info.primary_value.property_.lower() if "humidity" in property_lower: @@ -221,14 +225,72 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) + +class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): + """Representation of a Z-Wave Meter CC sensor.""" + + _attr_state_class = STATE_CLASS_MEASUREMENT + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveNumericSensor entity.""" + super().__init__(config_entry, client, info) + + # Entity class attributes + self._attr_last_reset = dt.utc_from_timestamp(0) + self._attr_device_class = DEVICE_CLASS_POWER + if self.info.primary_value.metadata.unit == "kWh": + self._attr_device_class = DEVICE_CLASS_ENERGY + + @callback + def async_update_last_reset( + self, node: ZwaveNode, endpoint: int, meter_type: int | None + ) -> None: + """Update last reset.""" + # If the signal is not for this node or is for a different endpoint, ignore it + if self.info.node != node or self.info.primary_value.endpoint != endpoint: + return + # If a meter type was specified and doesn't match this entity's meter type, + # ignore it + if ( + meter_type is not None + and self.info.primary_value.metadata.cc_specific.get("meterType") + != meter_type + ): + return + + self._attr_last_reset = dt.utcnow() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + await super().async_added_to_hass() + + # Restore the last reset time from stored state + restored_state = await self.async_get_last_state() + if restored_state and ATTR_LAST_RESET in restored_state.attributes: + self._attr_last_reset = dt.parse_datetime( + restored_state.attributes[ATTR_LAST_RESET] + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{SERVICE_RESET_METER}", + self.async_update_last_reset, + ) + ) + async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None ) -> None: """Reset meter(s) on device.""" node = self.info.node primary_value = self.info.primary_value - if primary_value.command_class != CommandClass.METER: - raise TypeError("Reset only available for Meter sensors") options = {} if meter_type is not None: options["type"] = meter_type @@ -244,6 +306,15 @@ class ZWaveNumericSensor(ZwaveSensorBase): primary_value.endpoint, options, ) + self._attr_last_reset = dt.utcnow() + # Notify meters that may have been reset + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{SERVICE_RESET_METER}", + node, + primary_value.endpoint, + options.get("type"), + ) class ZWaveListSensor(ZwaveSensorBase): diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 7177134aa33..c7100b22bd5 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,4 +1,6 @@ """Provide common test tools for Z-Wave JS.""" +from datetime import datetime, timezone + AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" @@ -29,3 +31,7 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = ( "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode" ) ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" +METER_SENSOR = "sensor.smart_switch_6_electric_consumed_v" + +DATETIME_ZERO = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc) +DATETIME_LAST_RESET = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 9dc8490b314..75b5ab65d38 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -11,6 +11,11 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo +from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.core import State + +from .common import DATETIME_LAST_RESET + from tests.common import MockConfigEntry, load_fixture # Add-on fixtures @@ -835,3 +840,16 @@ def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): def firmware_file_fixture(): """Return mock firmware file stream.""" return io.BytesIO(bytes(10)) + + +@pytest.fixture(name="restore_last_reset") +def restore_last_reset_fixture(): + """Return mock restore last reset.""" + state = State( + "sensor.test", "test", {ATTR_LAST_RESET: DATETIME_LAST_RESET.isoformat()} + ) + with patch( + "homeassistant.components.zwave_js.sensor.ZWaveMeterSensor.async_get_last_state", + return_value=state, + ): + yield state diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index aae7a1c0602..29fed2b5c55 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,6 +1,9 @@ """Test the Z-Wave JS sensor platform.""" +from unittest.mock import patch + from zwave_js_server.event import Event +from homeassistant.components.sensor import ATTR_LAST_RESET from homeassistant.components.zwave_js.const import ( ATTR_METER_TYPE, ATTR_VALUE, @@ -22,10 +25,13 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, BASIC_SENSOR, + DATETIME_LAST_RESET, + DATETIME_ZERO, ENERGY_SENSOR, HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, INDICATOR_SENSOR, + METER_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, ) @@ -171,18 +177,30 @@ async def test_reset_meter( integration, ): """Test reset_meter service.""" - SENSOR = "sensor.smart_switch_6_electric_consumed_v" client.async_send_command.return_value = {} client.async_send_command_no_wait.return_value = {} - # Test successful meter reset call - await hass.services.async_call( - DOMAIN, - SERVICE_RESET_METER, - { - ATTR_ENTITY_ID: SENSOR, - }, - blocking=True, + # Validate that the sensor last reset is starting from nothing + assert ( + hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + == DATETIME_ZERO.isoformat() + ) + + # Test successful meter reset call, patching utcnow so we can make sure the last + # reset gets updated + with patch("homeassistant.util.dt.utcnow", return_value=DATETIME_LAST_RESET): + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: METER_SENSOR, + }, + blocking=True, + ) + + assert ( + hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + == DATETIME_LAST_RESET.isoformat() ) assert len(client.async_send_command_no_wait.call_args_list) == 1 @@ -199,7 +217,7 @@ async def test_reset_meter( DOMAIN, SERVICE_RESET_METER, { - ATTR_ENTITY_ID: SENSOR, + ATTR_ENTITY_ID: METER_SENSOR, ATTR_METER_TYPE: 1, ATTR_VALUE: 2, }, @@ -214,3 +232,17 @@ async def test_reset_meter( assert args["args"] == [{"type": 1, "targetValue": 2}] client.async_send_command_no_wait.reset_mock() + + +async def test_restore_last_reset( + hass, + client, + aeon_smart_switch_6, + restore_last_reset, + integration, +): + """Test restoring last_reset on setup.""" + assert ( + hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + == DATETIME_LAST_RESET.isoformat() + ) From db8aa4658aaac4578483a26020447731f25608f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jul 2021 11:07:52 -0500 Subject: [PATCH 726/818] Skip each ssdp listener that fails to bind (#53670) --- homeassistant/components/ssdp/__init__.py | 16 +++++- tests/components/ssdp/test_init.py | 62 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index f0ba8b7dcea..31ebb0d1a92 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -256,9 +256,21 @@ class Scanner: self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start ) - await asyncio.gather( - *(listener.async_start() for listener in self._ssdp_listeners) + results = await asyncio.gather( + *(listener.async_start() for listener in self._ssdp_listeners), + return_exceptions=True, ) + failed_listeners = [] + for idx, result in enumerate(results): + if isinstance(result, Exception): + _LOGGER.warning( + "Failed to setup listener for %s: %s", + self._ssdp_listeners[idx].source_ip, + result, + ) + failed_listeners.append(self._ssdp_listeners[idx]) + for listener in failed_listeners: + self._ssdp_listeners.remove(listener) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL ) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 568a2261fee..34ca1b7228e 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -790,3 +790,65 @@ async def test_async_detect_interfaces_setting_empty_route(hass): (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), (IPv4Address("192.168.1.5"), None), } + + +async def test_bind_failure_skips_adapter(hass, caplog): + """Test that an adapter with a bind failure is skipped.""" + mock_get_ssdp = { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + } + create_args = [] + did_search = 0 + + @callback + def _callback(*_): + nonlocal did_search + did_search += 1 + pass + + def _generate_failing_ssdp_listener(*args, **kwargs): + create_args.append([args, kwargs]) + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + if kwargs["source_ip"] == IPv6Address("2001:db8::"): + raise OSError + pass + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.async_get_ssdp", + return_value=mock_get_ssdp, + ), patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_failing_ssdp_listener, + ), patch( + "homeassistant.components.ssdp.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ): + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + argset = set() + for argmap in create_args: + argset.add((argmap[1].get("source_ip"), argmap[1].get("target_ip"))) + + assert argset == { + (IPv6Address("2001:db8::"), None), + (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), + (IPv4Address("192.168.1.5"), None), + } + assert "Failed to setup listener for" in caplog.text + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + assert did_search == 2 From d7768f13c1b4191027663c301dd22dce3f652944 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 29 Jul 2021 00:13:55 -0700 Subject: [PATCH 727/818] pyWeMo version bump (0.6.6) (#53671) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 3d051fcc6dc..21a7760741a 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.6.5"], + "requirements": ["pywemo==0.6.6"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 6f9381d19ca..0538ecc5180 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1977,7 +1977,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.5 +pywemo==0.6.6 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1f1ba01960..1d3d9d9d070 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1095,7 +1095,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.5 +pywemo==0.6.6 # homeassistant.components.wilight pywilight==0.0.70 From 268f0dd62f77ee660df3ac7edb6e4eade7e44a59 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Jul 2021 22:49:13 -0700 Subject: [PATCH 728/818] Bump nest to version 0.3.5 (#53672) Fix runtime type assertions, Fixes Issue #53652 --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 05cfa261ef4..6c9462e43db 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.4"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.5"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0538ecc5180..063d8d0b4fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -707,7 +707,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.4 +google-nest-sdm==0.3.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d3d9d9d070..062e5068c91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.4 +google-nest-sdm==0.3.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 43a89dc4522851aa85967b5eecf1c1bc8503c090 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Jul 2021 21:09:14 +0200 Subject: [PATCH 729/818] Fix `last_reset_topic` config replaces `state_topic` for sensor platform (#53677) --- homeassistant/components/mqtt/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 0c234fbbbea..239af7b450a 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -196,7 +196,7 @@ class MqttSensor(MqttEntity, SensorEntity): self.async_write_ha_state() if CONF_LAST_RESET_TOPIC in self._config: - topics["state_topic"] = { + topics["last_reset_topic"] = { "topic": self._config[CONF_LAST_RESET_TOPIC], "msg_callback": last_reset_message_received, "qos": self._config[CONF_QOS], From 9ad29ae75cac2500af655e7eeaf0ae29a20b5a03 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Jul 2021 21:08:53 +0200 Subject: [PATCH 730/818] Only disable a device if all associated config entries are disabled (#53681) --- homeassistant/helpers/device_registry.py | 11 +++++++ tests/helpers/test_device_registry.py | 42 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9f09bbbf642..b22b1740a4f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -670,6 +670,7 @@ def async_config_entry_disabled_by_changed( the config entry is disabled, enable devices in the registry that are associated with a config entry when the config entry is enabled and the devices are marked DISABLED_CONFIG_ENTRY. + Only disable a device if all associated config entries are disabled. """ devices = async_entries_for_config_entry(registry, config_entry.entry_id) @@ -681,10 +682,20 @@ def async_config_entry_disabled_by_changed( registry.async_update_device(device.id, disabled_by=None) return + enabled_config_entries = { + entry.entry_id + for entry in registry.hass.config_entries.async_entries() + if not entry.disabled_by + } + for device in devices: if device.disabled: # Device already disabled, do not overwrite continue + if len(device.config_entries) > 1 and device.config_entries.intersection( + enabled_config_entries + ): + continue registry.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 037e1aec8c2..557647c5c7f 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1253,3 +1253,45 @@ async def test_disable_config_entry_disables_devices(hass, registry): entry2 = registry.async_get(entry2.id) assert entry2.disabled assert entry2.disabled_by == device_registry.DISABLED_USER + + +async def test_only_disable_device_if_all_config_entries_are_disabled(hass, registry): + """Test that we only disable device if all related config entries are disabled.""" + config_entry1 = MockConfigEntry(domain="light") + config_entry1.add_to_hass(hass) + config_entry2 = MockConfigEntry(domain="light") + config_entry2.add_to_hass(hass) + + registry.async_get_or_create( + config_entry_id=config_entry1.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry1 = registry.async_get_or_create( + config_entry_id=config_entry2.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert len(entry1.config_entries) == 2 + assert not entry1.disabled + + await hass.config_entries.async_set_disabled_by( + config_entry1.entry_id, config_entries.DISABLED_USER + ) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert not entry1.disabled + + await hass.config_entries.async_set_disabled_by( + config_entry2.entry_id, config_entries.DISABLED_USER + ) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert entry1.disabled + assert entry1.disabled_by == device_registry.DISABLED_CONFIG_ENTRY + + await hass.config_entries.async_set_disabled_by(config_entry1.entry_id, None) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert not entry1.disabled From 7f314e17de58e8032ea42b893745359adfb36328 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 29 Jul 2021 20:57:46 +0200 Subject: [PATCH 731/818] Bump bimmer_connected to 0.7.16 to fix parking light issue (#53687) --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index aff9e4fd647..17aaa166942 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.15"], + "requirements": ["bimmer_connected==0.7.16"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 063d8d0b4fd..e9a46b9e079 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -367,7 +367,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.15 +bimmer_connected==0.7.16 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 062e5068c91..29938321ee3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.15 +bimmer_connected==0.7.16 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 6ced0153df57ab218b133a4cc304fece958a4deb Mon Sep 17 00:00:00 2001 From: Andrew55529 Date: Thu, 29 Jul 2021 19:31:32 +0300 Subject: [PATCH 732/818] Fix problem with telegram_bot (#53690) --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index c3f07e5269d..02629e695fc 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -848,7 +848,7 @@ class BaseTelegramBotEntity: if ( msg_data["from"].get("id") not in self.allowed_chat_ids - and msg_data["chat"].get("id") not in self.allowed_chat_ids + and msg_data["message"]["chat"].get("id") not in self.allowed_chat_ids ): # Neither from id nor chat id was in allowed_chat_ids, # origin is not allowed. From 1117158bd0442cfd6012044dc606305826487933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 29 Jul 2021 16:59:02 +0200 Subject: [PATCH 733/818] Surepetcare, bug fix (#53695) --- homeassistant/components/surepetcare/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index ca7b7378127..5a9ae733db0 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -175,7 +175,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Get the latest data and update the state.""" surepy_entity = self._spc.states[self._id] state = surepy_entity.raw_data()["status"] - self._attr_is_on = self._attr_available = bool(self.state) + self._attr_is_on = self._attr_available = bool(state) if state: self._attr_extra_state_attributes = { "device_rssi": f'{state["signal"]["device_rssi"]:.2f}', From aa179a1ad99bcf918b2e05eb3006b18edc9d59c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 29 Jul 2021 18:44:38 +0200 Subject: [PATCH 734/818] Energy round (#53696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Energy. Round cost Signed-off-by: Daniel Hjelseth Høyer * Energy. Round cost Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/energy/sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/energy/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 773abbbe6b9..1c42ea5a050 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -152,10 +152,12 @@ class EnergyCostSensor(SensorEntity): self._attr_state_class = STATE_CLASS_MEASUREMENT self._flow = flow self._last_energy_sensor_state: State | None = None + self._cur_value = 0.0 def _reset(self, energy_state: State) -> None: """Reset the cost sensor.""" self._attr_state = 0.0 + self._cur_value = 0.0 self._attr_last_reset = dt_util.utcnow() self._last_energy_sensor_state = energy_state self.async_write_ha_state() @@ -195,7 +197,6 @@ class EnergyCostSensor(SensorEntity): self._reset(energy_state) return - cur_value = cast(float, self._attr_state) if ( energy_state.attributes[ATTR_LAST_RESET] != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] @@ -205,7 +206,8 @@ class EnergyCostSensor(SensorEntity): else: # Update with newly incurred cost old_energy_value = float(self._last_energy_sensor_state.state) - self._attr_state = cur_value + (energy - old_energy_value) * energy_price + self._cur_value += (energy - old_energy_value) * energy_price + self._attr_state = round(self._cur_value, 2) self._last_energy_sensor_state = energy_state From 462e3a3d0d2d65a18904dc16dc57c0420ee961d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 29 Jul 2021 20:05:53 +0200 Subject: [PATCH 735/818] Integration. Add device class, last_reset, state_class (#53698) Co-authored-by: Franck Nijhof --- .../components/integration/sensor.py | 29 ++++- tests/components/integration/test_sensor.py | 116 ++++++++++++++++-- 2 files changed, 132 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 0ab4ac0d2c4..dea8970f4f7 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -4,8 +4,16 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_METHOD, CONF_NAME, @@ -20,6 +28,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -115,16 +124,26 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] + self._attr_state_class = STATE_CLASS_MEASUREMENT async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() + self._attr_last_reset = dt_util.utcnow() if state: try: self._state = Decimal(state.state) - except ValueError as err: + except (DecimalException, ValueError) as err: _LOGGER.warning("Could not restore last state: %s", err) + else: + last_reset = dt_util.parse_datetime( + state.attributes.get(ATTR_LAST_RESET, "") + ) + self._attr_last_reset = ( + last_reset if last_reset else dt_util.utc_from_timestamp(0) + ) + self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) @callback def calc_integration(event): @@ -143,7 +162,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_of_measurement = self._unit_template.format( "" if unit is None else unit ) - + if ( + self.device_class is None + and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + ): + self._attr_device_class = DEVICE_CLASS_ENERGY try: # integration as the Riemann integral of previous measures. area = 0 diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 3afa5c14c22..dd6bf980d0f 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -2,12 +2,22 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TIME_SECONDS, +) +from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import mock_restore_cache -async def test_state(hass): + +async def test_state(hass) -> None: """Test integration sensor state.""" config = { "sensor": { @@ -19,15 +29,25 @@ async def test_state(hass): } } - assert await async_setup_component(hass, "sensor", config) - - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1, {}) - await hass.async_block_till_done() - - now = dt_util.utcnow() + timedelta(seconds=3600) + now = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.states.async_set(entity_id, 1, {}, force_update=True) + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 1, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + assert state.attributes.get("last_reset") == now.isoformat() + assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert "device_class" not in state.attributes + + future_now = dt_util.utcnow() + timedelta(seconds=3600) + with patch("homeassistant.util.dt.utcnow", return_value=future_now): + hass.states.async_set( + entity_id, 1, {"device_class": DEVICE_CLASS_POWER}, force_update=True + ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") @@ -37,6 +57,82 @@ async def test_state(hass): assert round(float(state.state), config["sensor"]["round"]) == 1.0 assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY + assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert state.attributes.get("last_reset") == now.isoformat() + + +async def test_restore_state(hass: HomeAssistant) -> None: + """Test integration sensor state is restored correctly.""" + mock_restore_cache( + hass, + ( + State( + "sensor.integration", + "100.0", + { + "last_reset": "2019-10-06T21:00:00", + "device_class": DEVICE_CLASS_ENERGY, + }, + ), + ), + ) + + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "unit": ENERGY_KILO_WATT_HOUR, + "round": 2, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state + assert state.state == "100.00" + assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY + assert state.attributes.get("last_reset") == "2019-10-06T21:00:00" + + +async def test_restore_state_failed(hass: HomeAssistant) -> None: + """Test integration sensor state is restored correctly.""" + mock_restore_cache( + hass, + ( + State( + "sensor.integration", + "INVALID", + { + "last_reset": "2019-10-06T21:00:00.000000", + }, + ), + ), + ) + + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "unit": ENERGY_KILO_WATT_HOUR, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state + assert state.state == "0" + assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert state.attributes.get("last_reset") != "2019-10-06T21:00:00" + assert "device_class" not in state.attributes async def test_trapezoidal(hass): From c6f588fc081eb864c16bb88344dcb72db4f0bbe4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 29 Jul 2021 20:58:48 +0200 Subject: [PATCH 736/818] Revert "Add Automate Pulse Hub v2 support (#39501)" (#53704) --- .coveragerc | 6 - CODEOWNERS | 1 - homeassistant/components/automate/__init__.py | 36 ----- homeassistant/components/automate/base.py | 93 ----------- .../components/automate/config_flow.py | 37 ----- homeassistant/components/automate/const.py | 6 - homeassistant/components/automate/cover.py | 147 ------------------ homeassistant/components/automate/helpers.py | 46 ------ homeassistant/components/automate/hub.py | 89 ----------- .../components/automate/manifest.json | 13 -- .../components/automate/strings.json | 19 --- .../components/automate/translations/ca.json | 19 --- .../components/automate/translations/de.json | 19 --- .../components/automate/translations/en.json | 19 --- .../components/automate/translations/et.json | 19 --- .../components/automate/translations/he.json | 19 --- .../components/automate/translations/nl.json | 19 --- .../components/automate/translations/pl.json | 19 --- .../components/automate/translations/ru.json | 19 --- .../automate/translations/zh-Hant.json | 19 --- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/automate/__init__.py | 1 - tests/components/automate/test_config_flow.py | 69 -------- 25 files changed, 741 deletions(-) delete mode 100644 homeassistant/components/automate/__init__.py delete mode 100644 homeassistant/components/automate/base.py delete mode 100644 homeassistant/components/automate/config_flow.py delete mode 100644 homeassistant/components/automate/const.py delete mode 100644 homeassistant/components/automate/cover.py delete mode 100644 homeassistant/components/automate/helpers.py delete mode 100644 homeassistant/components/automate/hub.py delete mode 100644 homeassistant/components/automate/manifest.json delete mode 100644 homeassistant/components/automate/strings.json delete mode 100644 homeassistant/components/automate/translations/ca.json delete mode 100644 homeassistant/components/automate/translations/de.json delete mode 100644 homeassistant/components/automate/translations/en.json delete mode 100644 homeassistant/components/automate/translations/et.json delete mode 100644 homeassistant/components/automate/translations/he.json delete mode 100644 homeassistant/components/automate/translations/nl.json delete mode 100644 homeassistant/components/automate/translations/pl.json delete mode 100644 homeassistant/components/automate/translations/ru.json delete mode 100644 homeassistant/components/automate/translations/zh-Hant.json delete mode 100644 tests/components/automate/__init__.py delete mode 100644 tests/components/automate/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c3516739798..1f28a9a2aee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -75,12 +75,6 @@ omit = homeassistant/components/asuswrt/router.py homeassistant/components/aten_pe/* homeassistant/components/atome/* - homeassistant/components/automate/__init__.py - homeassistant/components/automate/base.py - homeassistant/components/automate/const.py - homeassistant/components/automate/cover.py - homeassistant/components/automate/helpers.py - homeassistant/components/automate/hub.py homeassistant/components/aurora/__init__.py homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py diff --git a/CODEOWNERS b/CODEOWNERS index c4cb6d242d0..29906631254 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -56,7 +56,6 @@ homeassistant/components/august/* @bdraco homeassistant/components/aurora/* @djtimca homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core -homeassistant/components/automate/* @sillyfrog homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @ahayworth @danielsjf diff --git a/homeassistant/components/automate/__init__.py b/homeassistant/components/automate/__init__.py deleted file mode 100644 index c4f34d96a05..00000000000 --- a/homeassistant/components/automate/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -"""The Automate Pulse Hub v2 integration.""" -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import DOMAIN -from .hub import PulseHub - -PLATFORMS = ["cover"] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Automate Pulse Hub v2 from a config entry.""" - hub = PulseHub(hass, entry) - - if not await hub.async_setup(): - return False - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = hub - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - hub = hass.data[DOMAIN][entry.entry_id] - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - if not await hub.async_reset(): - return False - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/automate/base.py b/homeassistant/components/automate/base.py deleted file mode 100644 index de37933e54d..00000000000 --- a/homeassistant/components/automate/base.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Base class for Automate Roller Blinds.""" -import logging - -import aiopulse2 - -from homeassistant.core import callback -from homeassistant.helpers import entity -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg - -from .const import AUTOMATE_ENTITY_REMOVE, DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AutomateBase(entity.Entity): - """Base representation of an Automate roller.""" - - def __init__(self, roller: aiopulse2.Roller) -> None: - """Initialize the roller.""" - self.roller = roller - - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - return self.roller.online and self.roller.hub.connected - - async def async_remove_and_unregister(self): - """Unregister from entity and device registry and call entity remove function.""" - _LOGGER.info("Removing %s %s", self.__class__.__name__, self.unique_id) - - ent_registry = await get_ent_reg(self.hass) - if self.entity_id in ent_registry.entities: - ent_registry.async_remove(self.entity_id) - - dev_registry = await get_dev_reg(self.hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, self.unique_id)}, connections=set() - ) - if device is not None: - dev_registry.async_update_device( - device.id, remove_config_entry_id=self.registry_entry.config_entry_id - ) - - await self.async_remove() - - async def async_added_to_hass(self): - """Entity has been added to hass.""" - self.roller.callback_subscribe(self.notify_update) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - AUTOMATE_ENTITY_REMOVE.format(self.roller.id), - self.async_remove_and_unregister, - ) - ) - - async def async_will_remove_from_hass(self): - """Entity being removed from hass.""" - self.roller.callback_unsubscribe(self.notify_update) - - @callback - def notify_update(self, roller: aiopulse2.Roller): - """Write updated device state information.""" - _LOGGER.debug( - "Device update notification received: %s (%r)", roller.id, roller.name - ) - self.async_write_ha_state() - - @property - def should_poll(self): - """Report that Automate entities do not need polling.""" - return False - - @property - def unique_id(self): - """Return the unique ID of this roller.""" - return self.roller.id - - @property - def name(self): - """Return the name of roller.""" - return self.roller.name - - @property - def device_info(self): - """Return the device info.""" - attrs = { - "identifiers": {(DOMAIN, self.roller.id)}, - } - return attrs diff --git a/homeassistant/components/automate/config_flow.py b/homeassistant/components/automate/config_flow.py deleted file mode 100644 index 45d3a5b9349..00000000000 --- a/homeassistant/components/automate/config_flow.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Config flow for Automate Pulse Hub v2 integration.""" -import logging - -import aiopulse2 -import voluptuous as vol - -from homeassistant import config_entries - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) - - -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Automate Pulse Hub v2.""" - - VERSION = 1 - - async def async_step_user(self, user_input=None): - """Handle the initial step once we have info from the user.""" - if user_input is not None: - try: - hub = aiopulse2.Hub(user_input["host"]) - await hub.test() - title = hub.name - except Exception: # pylint: disable=broad-except - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": "cannot_connect"}, - ) - - return self.async_create_entry(title=title, data=user_input) - - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/automate/const.py b/homeassistant/components/automate/const.py deleted file mode 100644 index 0c1dc1bd2e5..00000000000 --- a/homeassistant/components/automate/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for the Automate Pulse Hub v2 integration.""" - -DOMAIN = "automate" - -AUTOMATE_HUB_UPDATE = "automate_hub_update_{}" -AUTOMATE_ENTITY_REMOVE = "automate_entity_remove_{}" diff --git a/homeassistant/components/automate/cover.py b/homeassistant/components/automate/cover.py deleted file mode 100644 index 86dcda10adf..00000000000 --- a/homeassistant/components/automate/cover.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Support for Automate Roller Blinds.""" -import aiopulse2 - -from homeassistant.components.cover import ( - ATTR_POSITION, - DEVICE_CLASS_SHADE, - SUPPORT_CLOSE, - SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, - SUPPORT_OPEN_TILT, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, - SUPPORT_STOP_TILT, - CoverEntity, -) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from .base import AutomateBase -from .const import AUTOMATE_HUB_UPDATE, DOMAIN -from .helpers import async_add_automate_entities - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Automate Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] - - current = set() - - @callback - def async_add_automate_covers(): - async_add_automate_entities( - hass, AutomateCover, config_entry, current, async_add_entities - ) - - hub.cleanup_callbacks.append( - async_dispatcher_connect( - hass, - AUTOMATE_HUB_UPDATE.format(config_entry.entry_id), - async_add_automate_covers, - ) - ) - - -class AutomateCover(AutomateBase, CoverEntity): - """Representation of a Automate cover device.""" - - @property - def current_cover_position(self): - """Return the current position of the roller blind. - - None is unknown, 0 is closed, 100 is fully open. - """ - position = None - if self.roller.closed_percent is not None: - position = 100 - self.roller.closed_percent - return position - - @property - def current_cover_tilt_position(self): - """Return the current tilt of the roller blind. - - None is unknown, 0 is closed, 100 is fully open. - """ - return None - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = 0 - if self.current_cover_position is not None: - supported_features |= ( - SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION - ) - if self.current_cover_tilt_position is not None: - supported_features |= ( - SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT - | SUPPORT_SET_TILT_POSITION - ) - - return supported_features - - @property - def device_info(self): - """Return the device info.""" - attrs = super().device_info - attrs["manufacturer"] = "Automate" - attrs["model"] = self.roller.devicetype - attrs["sw_version"] = self.roller.version - attrs["via_device"] = (DOMAIN, self.roller.hub.id) - attrs["name"] = self.name - return attrs - - @property - def device_class(self): - """Class of the cover, a shade.""" - return DEVICE_CLASS_SHADE - - @property - def is_opening(self): - """Is cover opening/moving up.""" - return self.roller.action == aiopulse2.MovingAction.up - - @property - def is_closing(self): - """Is cover closing/moving down.""" - return self.roller.action == aiopulse2.MovingAction.down - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self.roller.closed_percent == 100 - - async def async_close_cover(self, **kwargs): - """Close the roller.""" - await self.roller.move_down() - - async def async_open_cover(self, **kwargs): - """Open the roller.""" - await self.roller.move_up() - - async def async_stop_cover(self, **kwargs): - """Stop the roller.""" - await self.roller.move_stop() - - async def async_set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - await self.roller.move_to(100 - kwargs[ATTR_POSITION]) - - async def async_close_cover_tilt(self, **kwargs): - """Close the roller.""" - await self.roller.move_down() - - async def async_open_cover_tilt(self, **kwargs): - """Open the roller.""" - await self.roller.move_up() - - async def async_stop_cover_tilt(self, **kwargs): - """Stop the roller.""" - await self.roller.move_stop() - - async def async_set_cover_tilt(self, **kwargs): - """Tilt the roller shutter to a specific position.""" - await self.roller.move_to(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/automate/helpers.py b/homeassistant/components/automate/helpers.py deleted file mode 100644 index 92130eeb79b..00000000000 --- a/homeassistant/components/automate/helpers.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Helper functions for Automate Pulse.""" -import logging - -from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -@callback -def async_add_automate_entities( - hass, entity_class, config_entry, current, async_add_entities -): - """Add any new entities.""" - hub = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) - - api = hub.api.rollers - - new_items = [] - for unique_id, roller in api.items(): - if unique_id not in current: - _LOGGER.debug("New %s %s", entity_class.__name__, unique_id) - new_item = entity_class(roller) - current.add(unique_id) - new_items.append(new_item) - - async_add_entities(new_items) - - -async def update_devices(hass, config_entry, api): - """Tell hass that device info has been updated.""" - dev_registry = await get_dev_reg(hass) - - for api_item in api.values(): - # Update Device name - device = dev_registry.async_get_device( - identifiers={(DOMAIN, api_item.id)}, connections=set() - ) - if device is not None: - dev_registry.async_update_device( - device.id, - name=api_item.name, - ) diff --git a/homeassistant/components/automate/hub.py b/homeassistant/components/automate/hub.py deleted file mode 100644 index 78e1b5873fa..00000000000 --- a/homeassistant/components/automate/hub.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Code to handle a Pulse Hub.""" -from __future__ import annotations - -import asyncio -import logging - -import aiopulse2 - -from homeassistant.helpers.dispatcher import async_dispatcher_send - -from .const import AUTOMATE_ENTITY_REMOVE, AUTOMATE_HUB_UPDATE -from .helpers import update_devices - -_LOGGER = logging.getLogger(__name__) - - -class PulseHub: - """Manages a single Pulse Hub.""" - - def __init__(self, hass, config_entry): - """Initialize the system.""" - self.config_entry = config_entry - self.hass = hass - self.api: aiopulse2.Hub | None = None - self.tasks = [] - self.current_rollers = {} - self.cleanup_callbacks = [] - - @property - def title(self): - """Return the title of the hub shown in the integrations list.""" - return f"{self.api.name} ({self.api.host})" - - @property - def host(self): - """Return the host of this hub.""" - return self.config_entry.data["host"] - - async def async_setup(self): - """Set up a hub based on host parameter.""" - host = self.host - - hub = aiopulse2.Hub(host, propagate_callbacks=True) - - self.api = hub - - hub.callback_subscribe(self.async_notify_update) - self.tasks.append(asyncio.create_task(hub.run())) - - _LOGGER.debug("Hub setup complete") - return True - - async def async_reset(self): - """Reset this hub to default state.""" - for cleanup_callback in self.cleanup_callbacks: - cleanup_callback() - - # If not setup - if self.api is None: - return False - - self.api.callback_unsubscribe(self.async_notify_update) - await self.api.stop() - del self.api - self.api = None - - # Wait for any running tasks to complete - await asyncio.wait(self.tasks) - - return True - - async def async_notify_update(self, hub=None): - """Evaluate entities when hub reports that update has occurred.""" - _LOGGER.debug("Hub {self.title} updated") - - await update_devices(self.hass, self.config_entry, self.api.rollers) - self.hass.config_entries.async_update_entry(self.config_entry, title=self.title) - - async_dispatcher_send( - self.hass, AUTOMATE_HUB_UPDATE.format(self.config_entry.entry_id) - ) - - for unique_id in list(self.current_rollers): - if unique_id not in self.api.rollers: - _LOGGER.debug("Notifying remove of %s", unique_id) - self.current_rollers.pop(unique_id) - async_dispatcher_send( - self.hass, AUTOMATE_ENTITY_REMOVE.format(unique_id) - ) diff --git a/homeassistant/components/automate/manifest.json b/homeassistant/components/automate/manifest.json deleted file mode 100644 index 071aaf1589f..00000000000 --- a/homeassistant/components/automate/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "automate", - "name": "Automate Pulse Hub v2", - "config_flow": true, - "iot_class": "local_push", - "documentation": "https://www.home-assistant.io/integrations/automate", - "requirements": [ - "aiopulse2==0.6.0" - ], - "codeowners": [ - "@sillyfrog" - ] -} \ No newline at end of file diff --git a/homeassistant/components/automate/strings.json b/homeassistant/components/automate/strings.json deleted file mode 100644 index 8a8131f0f67..00000000000 --- a/homeassistant/components/automate/strings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "host": "[%key:common::config_flow::data::host%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/ca.json b/homeassistant/components/automate/translations/ca.json deleted file mode 100644 index 69cd7de20aa..00000000000 --- a/homeassistant/components/automate/translations/ca.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "host": "Amfitri\u00f3" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/de.json b/homeassistant/components/automate/translations/de.json deleted file mode 100644 index fa773dbf708..00000000000 --- a/homeassistant/components/automate/translations/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/en.json b/homeassistant/components/automate/translations/en.json deleted file mode 100644 index 2ad35962b25..00000000000 --- a/homeassistant/components/automate/translations/en.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/et.json b/homeassistant/components/automate/translations/et.json deleted file mode 100644 index da2c8eef4d2..00000000000 --- a/homeassistant/components/automate/translations/et.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" - }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamine nurjus", - "unknown": "Ootamatu t\u00f5rge" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/he.json b/homeassistant/components/automate/translations/he.json deleted file mode 100644 index accc8a0a610..00000000000 --- a/homeassistant/components/automate/translations/he.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, - "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/nl.json b/homeassistant/components/automate/translations/nl.json deleted file mode 100644 index bb92bf9e593..00000000000 --- a/homeassistant/components/automate/translations/nl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd" - }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/pl.json b/homeassistant/components/automate/translations/pl.json deleted file mode 100644 index 647c0921e3a..00000000000 --- a/homeassistant/components/automate/translations/pl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "host": "Nazwa hosta lub adres IP" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/ru.json b/homeassistant/components/automate/translations/ru.json deleted file mode 100644 index 1212d054ff7..00000000000 --- a/homeassistant/components/automate/translations/ru.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." - }, - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/zh-Hant.json b/homeassistant/components/automate/translations/zh-Hant.json deleted file mode 100644 index 0fbaa19828f..00000000000 --- a/homeassistant/components/automate/translations/zh-Hant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "host": "\u4e3b\u6a5f\u7aef" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 89b1a9ba8ae..4cb9e2e3c4b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,7 +28,6 @@ FLOWS = [ "atag", "august", "aurora", - "automate", "awair", "axis", "azure_devops", diff --git a/requirements_all.txt b/requirements_all.txt index e9a46b9e079..96d5c1c3360 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,9 +220,6 @@ aionotify==0.2.0 # homeassistant.components.notion aionotion==3.0.2 -# homeassistant.components.automate -aiopulse2==0.6.0 - # homeassistant.components.acmeda aiopulse==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29938321ee3..a5f527a05e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -142,9 +142,6 @@ aiomusiccast==0.8.2 # homeassistant.components.notion aionotion==3.0.2 -# homeassistant.components.automate -aiopulse2==0.6.0 - # homeassistant.components.acmeda aiopulse==0.4.2 diff --git a/tests/components/automate/__init__.py b/tests/components/automate/__init__.py deleted file mode 100644 index 6a87ba942e3..00000000000 --- a/tests/components/automate/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Automate Pulse Hub v2 integration.""" diff --git a/tests/components/automate/test_config_flow.py b/tests/components/automate/test_config_flow.py deleted file mode 100644 index fea2fa995cd..00000000000 --- a/tests/components/automate/test_config_flow.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Test the Automate Pulse Hub v2 config flow.""" -from unittest.mock import Mock, patch - -from homeassistant import config_entries, setup -from homeassistant.components.automate.const import DOMAIN - - -def mock_hub(testfunc=None): - """Mock aiopulse2.Hub.""" - Hub = Mock() - Hub.name = "Name of the device" - - async def hub_test(): - if testfunc: - testfunc() - - Hub.test = hub_test - - return Hub - - -async def test_form(hass): - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] is None - - with patch("aiopulse2.Hub", return_value=mock_hub()), patch( - "homeassistant.components.automate.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - }, - ) - - assert result2["type"] == "create_entry" - assert result2["title"] == "Name of the device" - assert result2["data"] == { - "host": "1.1.1.1", - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - def raise_error(): - raise ConnectionRefusedError - - with patch("aiopulse2.Hub", return_value=mock_hub(raise_error)): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} From b2187022c49d84f1db4a2302458f124575a12d4a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 Jul 2021 20:54:51 +0200 Subject: [PATCH 737/818] Set state class measurement also for Total Energy for AVM Fritz!Smarthome devices (#53707) --- homeassistant/components/fritzbox/sensor.py | 2 +- tests/components/fritzbox/test_switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 24b3d9cc5ff..d325e592faf 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}_total_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: None, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, coordinator, ain, diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index cb6ae85f889..951528f1e7d 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -76,7 +76,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_LAST_RESET] == "1970-01-01T00:00:00+00:00" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert ATTR_STATE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT async def test_turn_on(hass: HomeAssistant, fritz: Mock): From 4b2a1ec694998195b16946f54498b8e42ee32b6f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 29 Jul 2021 21:10:53 +0200 Subject: [PATCH 738/818] Add last reset to Shelly's energy entities (#53710) --- homeassistant/components/shelly/const.py | 3 +++ homeassistant/components/shelly/entity.py | 4 ++-- homeassistant/components/shelly/sensor.py | 21 +++++++++++++++++---- homeassistant/components/shelly/utils.py | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 2c401829c30..49e33dfd5e1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -92,3 +92,6 @@ KELVIN_MIN_VALUE_WHITE: Final = 2700 KELVIN_MIN_VALUE_COLOR: Final = 3000 UPTIME_DEVIATION: Final = 5 + +LAST_RESET_UPTIME: Final = "uptime" +LAST_RESET_NEVER: Final = "never" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index c8b23f71bd7..a1ce2e671d1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from datetime import datetime import logging from typing import Any, Callable, Final, cast @@ -180,7 +179,7 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None - last_reset: datetime | None = None + last_reset: str | None = None @dataclass @@ -286,6 +285,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit: None | str | Callable[[dict], str] = unit self._unique_id: str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) + self._last_value: str | None = None @property def unique_id(self) -> str: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7c2ffdbc470..56e4f63bc75 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt -from .const import SHAIR_MAX_WORK_HOURS +from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -114,6 +114,7 @@ SENSORS: Final = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=LAST_RESET_UPTIME, ), ("emeter", "energy"): BlockAttributeDescription( name="Energy", @@ -121,7 +122,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + last_reset=LAST_RESET_NEVER, ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", @@ -129,7 +130,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + last_reset=LAST_RESET_NEVER, ), ("light", "energy"): BlockAttributeDescription( name="Energy", @@ -138,6 +139,7 @@ SENSORS: Final = { device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, + last_reset=LAST_RESET_UPTIME, ), ("relay", "energy"): BlockAttributeDescription( name="Energy", @@ -145,6 +147,7 @@ SENSORS: Final = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=LAST_RESET_UPTIME, ), ("roller", "rollerEnergy"): BlockAttributeDescription( name="Energy", @@ -152,6 +155,7 @@ SENSORS: Final = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=LAST_RESET_UPTIME, ), ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", @@ -264,7 +268,16 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): @property def last_reset(self) -> datetime | None: """State class of sensor.""" - return self.description.last_reset + if self.description.last_reset == LAST_RESET_UPTIME: + self._last_value = get_device_uptime( + self.wrapper.device.status, self._last_value + ) + return dt.parse_datetime(self._last_value) + + if self.description.last_reset == LAST_RESET_NEVER: + return dt.utc_from_timestamp(0) + + return None @property def unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d8ce5ae9e45..d1e2947d5ac 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -136,7 +136,7 @@ def is_momentary_input(settings: dict[str, Any], block: aioshelly.Block) -> bool return button_type in ["momentary", "momentary_on_release"] -def get_device_uptime(status: dict[str, Any], last_uptime: str) -> str: +def get_device_uptime(status: dict[str, Any], last_uptime: str | None) -> str: """Return device uptime string, tolerate up to 5 seconds deviation.""" delta_uptime = utcnow() - timedelta(seconds=status["uptime"]) From dc2494f0a0318d2a643b929308450b021145c8bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 21:25:18 +0200 Subject: [PATCH 739/818] Add state class support to DSMR Reader (#53715) --- .../components/dsmr_reader/definitions.py | 998 ++++++++++-------- .../components/dsmr_reader/sensor.py | 74 +- 2 files changed, 553 insertions(+), 519 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 51aaca24c02..1a46f86132b 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -1,5 +1,13 @@ """Definitions for DSMR Reader sensors added to MQTT.""" +from __future__ import annotations +from dataclasses import dataclass +from typing import Callable + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_CURRENT, @@ -13,6 +21,7 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util def dsmr_transform(value): @@ -29,462 +38,533 @@ def tariff_transform(value): return "high" -DEFINITIONS = { - "dsmr/reading/electricity_delivered_1": { - "name": "Low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_returned_1": { - "name": "Low tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_delivered_2": { - "name": "High tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_returned_2": { - "name": "High tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_currently_delivered": { - "name": "Current power usage", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/electricity_currently_returned": { - "name": "Current power return", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_delivered_l1": { - "name": "Current power usage L1", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_delivered_l2": { - "name": "Current power usage L2", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_delivered_l3": { - "name": "Current power usage L3", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_returned_l1": { - "name": "Current power return L1", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_returned_l2": { - "name": "Current power return L2", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_returned_l3": { - "name": "Current power return L3", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/extra_device_delivered": { - "name": "Gas meter usage", - "enable_default": True, - "icon": "mdi:fire", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/reading/phase_voltage_l1": { - "name": "Current voltage L1", - "enable_default": True, - "device_class": DEVICE_CLASS_VOLTAGE, - "unit": ELECTRIC_POTENTIAL_VOLT, - }, - "dsmr/reading/phase_voltage_l2": { - "name": "Current voltage L2", - "enable_default": True, - "device_class": DEVICE_CLASS_VOLTAGE, - "unit": ELECTRIC_POTENTIAL_VOLT, - }, - "dsmr/reading/phase_voltage_l3": { - "name": "Current voltage L3", - "enable_default": True, - "device_class": DEVICE_CLASS_VOLTAGE, - "unit": ELECTRIC_POTENTIAL_VOLT, - }, - "dsmr/reading/phase_power_current_l1": { - "name": "Phase power current L1", - "enable_default": True, - "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRIC_CURRENT_AMPERE, - }, - "dsmr/reading/phase_power_current_l2": { - "name": "Phase power current L2", - "enable_default": True, - "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRIC_CURRENT_AMPERE, - }, - "dsmr/reading/phase_power_current_l3": { - "name": "Phase power current L3", - "enable_default": True, - "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRIC_CURRENT_AMPERE, - }, - "dsmr/reading/timestamp": { - "name": "Telegram timestamp", - "enable_default": False, - "device_class": DEVICE_CLASS_TIMESTAMP, - }, - "dsmr/consumption/gas/delivered": { - "name": "Gas usage", - "enable_default": True, - "icon": "mdi:fire", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/consumption/gas/currently_delivered": { - "name": "Current gas usage", - "enable_default": True, - "icon": "mdi:fire", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/consumption/gas/read_at": { - "name": "Gas meter read", - "enable_default": True, - "device_class": DEVICE_CLASS_TIMESTAMP, - }, - "dsmr/day-consumption/electricity1": { - "name": "Low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity2": { - "name": "High tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity1_returned": { - "name": "Low tariff return", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity2_returned": { - "name": "High tariff return", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity_merged": { - "name": "Power usage total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity_returned_merged": { - "name": "Power return total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity1_cost": { - "name": "Low tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/electricity2_cost": { - "name": "High tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/electricity_cost_merged": { - "name": "Power total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/gas": { - "name": "Gas usage", - "enable_default": True, - "icon": "mdi:counter", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/day-consumption/gas_cost": { - "name": "Gas cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/total_cost": { - "name": "Total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { - "name": "Low tariff delivered price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { - "name": "High tariff delivered price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { - "name": "Low tariff returned price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { - "name": "High tariff returned price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_gas": { - "name": "Gas price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/fixed_cost": { - "name": "Current day fixed cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/meter-stats/dsmr_version": { - "name": "DSMR version", - "enable_default": True, - "icon": "mdi:alert-circle", - "transform": dsmr_transform, - }, - "dsmr/meter-stats/electricity_tariff": { - "name": "Electricity tariff", - "enable_default": True, - "icon": "mdi:flash", - "transform": tariff_transform, - }, - "dsmr/meter-stats/power_failure_count": { - "name": "Power failure count", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/long_power_failure_count": { - "name": "Long power failure count", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_sag_count_l1": { - "name": "Voltage sag L1", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_sag_count_l2": { - "name": "Voltage sag L2", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_sag_count_l3": { - "name": "Voltage sag L3", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_swell_count_l1": { - "name": "Voltage swell L1", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_swell_count_l2": { - "name": "Voltage swell L2", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_swell_count_l3": { - "name": "Voltage swell L3", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/rejected_telegrams": { - "name": "Rejected telegrams", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/current-month/electricity1": { - "name": "Current month low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity2": { - "name": "Current month high tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity1_returned": { - "name": "Current month low tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity2_returned": { - "name": "Current month high tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity_merged": { - "name": "Current month power usage total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity_returned_merged": { - "name": "Current month power return total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity1_cost": { - "name": "Current month low tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/electricity2_cost": { - "name": "Current month high tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/electricity_cost_merged": { - "name": "Current month power total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/gas": { - "name": "Current month gas usage", - "enable_default": True, - "icon": "mdi:counter", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/current-month/gas_cost": { - "name": "Current month gas cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/fixed_cost": { - "name": "Current month fixed cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/total_cost": { - "name": "Current month total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/electricity1": { - "name": "Current year low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity2": { - "name": "Current year high tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity1_returned": { - "name": "Current year low tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity2_returned": { - "name": "Current year high tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity_merged": { - "name": "Current year power usage total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity_returned_merged": { - "name": "Current year power returned total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity1_cost": { - "name": "Current year low tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/electricity2_cost": { - "name": "Current year high tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/electricity_cost_merged": { - "name": "Current year power total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/gas": { - "name": "Current year gas usage", - "enable_default": True, - "icon": "mdi:counter", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/current-year/gas_cost": { - "name": "Current year gas cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/fixed_cost": { - "name": "Current year fixed cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/total_cost": { - "name": "Current year total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, -} +@dataclass +class DSMRReaderSensorEntityDescription(SensorEntityDescription): + """Sensor entity description for DSMR Reader.""" + + state: Callable | None = None + + +SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_delivered_1", + name="Low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_returned_1", + name="Low tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_delivered_2", + name="High tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_returned_2", + name="High tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_currently_delivered", + name="Current power usage", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_currently_returned", + name="Current power return", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_delivered_l1", + name="Current power usage L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_delivered_l2", + name="Current power usage L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_delivered_l3", + name="Current power usage L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_returned_l1", + name="Current power return L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_returned_l2", + name="Current power return L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_returned_l3", + name="Current power return L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/extra_device_delivered", + name="Gas meter usage", + entity_registry_enabled_default=False, + icon="mdi:fire", + unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_voltage_l1", + name="Current voltage L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_voltage_l2", + name="Current voltage L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_voltage_l3", + name="Current voltage L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_power_current_l1", + name="Phase power current L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_power_current_l2", + name="Phase power current L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_power_current_l3", + name="Phase power current L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/timestamp", + name="Telegram timestamp", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_TIMESTAMP, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/consumption/gas/delivered", + name="Gas usage", + icon="mdi:fire", + unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/consumption/gas/currently_delivered", + name="Current gas usage", + icon="mdi:fire", + unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/consumption/gas/read_at", + name="Gas meter read", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_TIMESTAMP, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity1", + name="Low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity2", + name="High tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity1_returned", + name="Low tariff return", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity2_returned", + name="High tariff return", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity_merged", + name="Power usage total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity_returned_merged", + name="Power return total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity1_cost", + name="Low tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity2_cost", + name="High tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity_cost_merged", + name="Power total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/gas", + name="Gas usage", + icon="mdi:counter", + unit_of_measurement=VOLUME_CUBIC_METERS, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/gas_cost", + name="Gas cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/total_cost", + name="Total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_1", + name="Low tariff delivered price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_2", + name="High tariff delivered price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_returned_1", + name="Low tariff returned price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_returned_2", + name="High tariff returned price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_gas", + name="Gas price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/fixed_cost", + name="Current day fixed cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/dsmr_version", + name="DSMR version", + entity_registry_enabled_default=False, + icon="mdi:alert-circle", + state=dsmr_transform, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/electricity_tariff", + name="Electricity tariff", + icon="mdi:flash", + state=tariff_transform, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/power_failure_count", + name="Power failure count", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/long_power_failure_count", + name="Long power failure count", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_sag_count_l1", + name="Voltage sag L1", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_sag_count_l2", + name="Voltage sag L2", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_sag_count_l3", + name="Voltage sag L3", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_swell_count_l1", + name="Voltage swell L1", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_swell_count_l2", + name="Voltage swell L2", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_swell_count_l3", + name="Voltage swell L3", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/rejected_telegrams", + name="Rejected telegrams", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity1", + name="Current month low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity2", + name="Current month high tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity1_returned", + name="Current month low tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity2_returned", + name="Current month high tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity_merged", + name="Current month power usage total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity_returned_merged", + name="Current month power return total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity1_cost", + name="Current month low tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity2_cost", + name="Current month high tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity_cost_merged", + name="Current month power total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/gas", + name="Current month gas usage", + icon="mdi:counter", + unit_of_measurement=VOLUME_CUBIC_METERS, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/gas_cost", + name="Current month gas cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/fixed_cost", + name="Current month fixed cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/total_cost", + name="Current month total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity1", + name="Current year low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity2", + name="Current year high tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity1_returned", + name="Current year low tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity2_returned", + name="Current year high tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity_merged", + name="Current year power usage total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity_returned_merged", + name="Current year power returned total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity1_cost", + name="Current year low tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity2_cost", + name="Current year high tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity_cost_merged", + name="Current year power total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/gas", + name="Current year gas usage", + icon="mdi:counter", + unit_of_measurement=VOLUME_CUBIC_METERS, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/gas_cost", + name="Current year gas cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/fixed_cost", + name="Current year fixed cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/total_cost", + name="Current year total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), +) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 0ee5932c1bb..39356db46b5 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -4,39 +4,27 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.util import slugify -from .definitions import DEFINITIONS +from .definitions import SENSORS, DSMRReaderSensorEntityDescription DOMAIN = "dsmr_reader" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up DSMR Reader sensors.""" - - sensors = [] - for topic in DEFINITIONS: - sensors.append(DSMRSensor(topic)) - - async_add_entities(sensors) + async_add_entities(DSMRSensor(description) for description in SENSORS) class DSMRSensor(SensorEntity): """Representation of a DSMR sensor that is updated via MQTT.""" - def __init__(self, topic): + entity_description: DSMRReaderSensorEntityDescription + + def __init__(self, description: DSMRReaderSensorEntityDescription) -> None: """Initialize the sensor.""" + self.entity_description = description - self._definition = DEFINITIONS[topic] - - self._entity_id = slugify(topic.replace("/", "_")) - self._topic = topic - - self._name = self._definition.get("name", topic.split("/")[-1]) - self._device_class = self._definition.get("device_class") - self._enable_default = self._definition.get("enable_default") - self._unit_of_measurement = self._definition.get("unit") - self._icon = self._definition.get("icon") - self._transform = self._definition.get("transform") - self._state = None + slug = slugify(description.key.replace("/", "_")) + self.entity_id = f"sensor.{slug}" async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -44,47 +32,13 @@ class DSMRSensor(SensorEntity): @callback def message_received(message): """Handle new MQTT messages.""" - - if self._transform is not None: - self._state = self._transform(message.payload) + if self.entity_description.state is not None: + self._attr_state = self.entity_description.state(message.payload) else: - self._state = message.payload + self._attr_state = message.payload self.async_write_ha_state() - await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) - - @property - def name(self): - """Return the name of the sensor supplied in constructor.""" - return self._name - - @property - def entity_id(self): - """Return the entity ID for this sensor.""" - return f"sensor.{self._entity_id}" - - @property - def state(self): - """Return the current state of the entity.""" - return self._state - - @property - def device_class(self): - """Return the device_class of this sensor.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of this sensor.""" - return self._unit_of_measurement - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enable_default - - @property - def icon(self): - """Return the icon of this sensor.""" - return self._icon + await mqtt.async_subscribe( + self.hass, self.entity_description.key, message_received, 1 + ) From cc0aa32f3e2fa77082794f5cd917220980a8babf Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 29 Jul 2021 21:25:43 +0200 Subject: [PATCH 740/818] Fix zwave_js meter sensor state class (#53716) --- homeassistant/components/zwave_js/sensor.py | 3 +-- tests/components/zwave_js/test_sensor.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 39aa2f30604..f4b303aa1af 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -229,8 +229,6 @@ class ZWaveNumericSensor(ZwaveSensorBase): class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): """Representation of a Z-Wave Meter CC sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT - def __init__( self, config_entry: ConfigEntry, @@ -241,6 +239,7 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): super().__init__(config_entry, client, info) # Entity class attributes + self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_last_reset = dt.utc_from_timestamp(0) self._attr_device_class = DEVICE_CLASS_POWER if self.info.primary_value.metadata.unit == "kWh": diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 29fed2b5c55..ebe406fb951 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch from zwave_js_server.event import Event -from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.components.sensor import ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT from homeassistant.components.zwave_js.const import ( ATTR_METER_TYPE, ATTR_VALUE, @@ -62,6 +62,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.state == "0.0" assert state.attributes["unit_of_measurement"] == POWER_WATT assert state.attributes["device_class"] == DEVICE_CLASS_POWER + assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT state = hass.states.get(ENERGY_SENSOR) @@ -69,6 +70,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.state == "0.16" assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY + assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT async def test_disabled_notification_sensor(hass, multisensor_6, integration): From b1758e1fcc351df2603d730f84bf8ee047b54715 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 12:34:32 -0700 Subject: [PATCH 741/818] Bump frontend to 20210729.0 (#53717) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cf835396fba..ac791977038 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210728.0" + "home-assistant-frontend==20210729.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0556c6e5452..e1acf1dba15 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210728.0 +home-assistant-frontend==20210729.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 96d5c1c3360..b7f8ec34eda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210728.0 +home-assistant-frontend==20210729.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5f527a05e6..e8b1a136048 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210728.0 +home-assistant-frontend==20210729.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From bf6133534dfe27e9557fb8482b11592156011c31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 21:34:22 +0200 Subject: [PATCH 742/818] Fix SolarEdge statistics; missing device_class (#53720) --- homeassistant/components/solaredge/const.py | 57 ++++++++++++-------- homeassistant/components/solaredge/models.py | 16 ++---- homeassistant/components/solaredge/sensor.py | 45 ++++++++-------- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 81d9bc5aebe..872781bf19c 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -3,10 +3,16 @@ from datetime import timedelta import logging from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) from homeassistant.util import dt as dt_util -from .models import SolarEdgeSensor +from .models import SolarEdgeSensorEntityDescription DOMAIN = "solaredge" @@ -29,7 +35,7 @@ SCAN_INTERVAL = timedelta(minutes=15) # Supported overview sensors SENSOR_TYPES = [ - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="lifetime_energy", json_key="lifeTimeData", name="Lifetime energy", @@ -37,138 +43,143 @@ SENSOR_TYPES = [ last_reset=dt_util.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="energy_this_year", json_key="lastYearData", name="Energy this year", entity_registry_enabled_default=False, icon="mdi:solar-power", unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="energy_this_month", json_key="lastMonthData", name="Energy this month", entity_registry_enabled_default=False, icon="mdi:solar-power", unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="energy_today", json_key="lastDayData", name="Energy today", entity_registry_enabled_default=False, icon="mdi:solar-power", unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="current_power", json_key="currentPower", name="Current Power", icon="mdi:solar-power", state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="site_details", name="Site details", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="meters", json_key="meters", name="Meters", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="sensors", json_key="sensors", name="Sensors", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="gateways", json_key="gateways", name="Gateways", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="batteries", json_key="batteries", name="Batteries", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="inverters", json_key="inverters", name="Inverters", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="power_consumption", json_key="LOAD", name="Power Consumption", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="solar_power", json_key="PV", name="Solar Power", entity_registry_enabled_default=False, icon="mdi:solar-power", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="grid_power", json_key="GRID", name="Grid Power", entity_registry_enabled_default=False, icon="mdi:power-plug", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="storage_power", json_key="STORAGE", name="Storage Power", entity_registry_enabled_default=False, icon="mdi:car-battery", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="purchased_power", json_key="Purchased", name="Imported Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="production_power", json_key="Production", name="Production Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="consumption_power", json_key="Consumption", name="Consumption Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="selfconsumption_power", json_key="SelfConsumption", name="SelfConsumption Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="feedin_power", json_key="FeedIn", name="Exported Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="storage_level", json_key="STORAGE", name="Storage Level", diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py index f91db9ee9ff..ce24d854aac 100644 --- a/homeassistant/components/solaredge/models.py +++ b/homeassistant/components/solaredge/models.py @@ -2,20 +2,12 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime + +from homeassistant.components.sensor import SensorEntityDescription @dataclass -class SolarEdgeSensor: - """Represents an SolarEdge Sensor.""" - - key: str - name: str +class SolarEdgeSensorEntityDescription(SensorEntityDescription): + """Sensor entity description for SolarEdge.""" json_key: str | None = None - device_class: str | None = None - entity_registry_enabled_default: bool = True - icon: str | None = None - last_reset: datetime | None = None - state_class: str | None = None - unit_of_measurement: str | None = None diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 23cfa116599..85e01a2d7ee 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -21,7 +21,7 @@ from .coordinator import ( SolarEdgeOverviewDataService, SolarEdgePowerFlowDataService, ) -from .models import SolarEdgeSensor +from .models import SolarEdgeSensorEntityDescription async def async_setup_entry( @@ -68,7 +68,8 @@ class SolarEdgeSensorFactory: self.services: dict[ str, tuple[ - type[SolarEdgeSensor | SolarEdgeOverviewSensor], SolarEdgeDataService + type[SolarEdgeSensorEntity | SolarEdgeOverviewSensor], + SolarEdgeDataService, ], ] = {"site_details": (SolarEdgeDetailsSensor, details)} @@ -99,7 +100,9 @@ class SolarEdgeSensorFactory: ): self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) - def create_sensor(self, sensor_type: SolarEdgeSensor) -> SolarEdgeSensor: + def create_sensor( + self, sensor_type: SolarEdgeSensorEntityDescription + ) -> SolarEdgeSensorEntityDescription: """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_type.key] @@ -109,27 +112,21 @@ class SolarEdgeSensorFactory: class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity): """Abstract class for a solaredge sensor.""" + entity_description: SolarEdgeSensorEntityDescription + def __init__( self, platform_name: str, - sensor_type: SolarEdgeSensor, + description: SolarEdgeSensorEntityDescription, data_service: SolarEdgeDataService, ) -> None: """Initialize the sensor.""" super().__init__(data_service.coordinator) self.platform_name = platform_name - self.sensor_type = sensor_type + self.entity_description = description self.data_service = data_service - self._attr_device_class = sensor_type.device_class - self._attr_entity_registry_enabled_default = ( - sensor_type.entity_registry_enabled_default - ) - self._attr_icon = sensor_type.icon - self._attr_last_reset = sensor_type.last_reset - self._attr_name = f"{platform_name} ({sensor_type.name})" - self._attr_state_class = sensor_type.state_class - self._attr_unit_of_measurement = sensor_type.unit_of_measurement + self._attr_name = f"{platform_name} ({description.name})" class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): @@ -138,7 +135,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): @@ -161,12 +158,12 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self.sensor_type.json_key) + return self.data_service.attributes.get(self.entity_description.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): @@ -181,12 +178,12 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self.sensor_type.json_key) + return self.data_service.attributes.get(self.entity_description.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): @@ -197,23 +194,23 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): def __init__( self, platform_name: str, - sensor_type: SolarEdgeSensor, + description: SolarEdgeSensorEntityDescription, data_service: SolarEdgeDataService, ) -> None: """Initialize the power flow sensor.""" - super().__init__(platform_name, sensor_type, data_service) + super().__init__(platform_name, description, data_service) self._attr_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self.sensor_type.json_key) + return self.data_service.attributes.get(self.entity_description.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): @@ -224,7 +221,7 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): @property def state(self) -> str | None: """Return the state of the sensor.""" - attr = self.data_service.attributes.get(self.sensor_type.json_key) + attr = self.data_service.attributes.get(self.entity_description.json_key) if attr and "soc" in attr: return attr["soc"] return None From 6dc00d3d87afdfdd13c33a5ff76a2ea44c46e272 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 12:35:50 -0700 Subject: [PATCH 743/818] Bumped version to 2021.8.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 15d0a3485cd..c7778e23c67 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From f1400b03bbada0cda3b96690e79fa255eea18053 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 22:55:26 +0200 Subject: [PATCH 744/818] Fix DSMR reconnecting loop without timeout (#53722) --- homeassistant/components/dsmr/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index d5674b34520..5afc229a727 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -165,6 +165,9 @@ async def async_setup_entry( LOGGER.exception("Error connecting to DSMR") transport = None protocol = None + + # throttle reconnect attempts + await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) except CancelledError: if stop_listener: stop_listener() # pylint: disable=not-callable From 8cf0182f2fd0f06b07e7afc293a1040c467588da Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 29 Jul 2021 23:26:57 +0200 Subject: [PATCH 745/818] Fix zwave_js current and voltage meter sensor device class (#53723) --- homeassistant/components/zwave_js/sensor.py | 11 ++++++++--- tests/components/zwave_js/common.py | 4 +++- tests/components/zwave_js/test_sensor.py | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index f4b303aa1af..304a80f7940 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -22,8 +22,10 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -142,8 +144,14 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): return DEVICE_CLASS_HUMIDITY if "temperature" in property_lower: return DEVICE_CLASS_TEMPERATURE + if self.info.primary_value.metadata.unit == "A": + return DEVICE_CLASS_CURRENT if self.info.primary_value.metadata.unit == "W": return DEVICE_CLASS_POWER + if self.info.primary_value.metadata.unit == "kWh": + return DEVICE_CLASS_ENERGY + if self.info.primary_value.metadata.unit == "V": + return DEVICE_CLASS_VOLTAGE if self.info.primary_value.metadata.unit == "Lux": return DEVICE_CLASS_ILLUMINANCE return None @@ -241,9 +249,6 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): # Entity class attributes self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_last_reset = dt.utc_from_timestamp(0) - self._attr_device_class = DEVICE_CLASS_POWER - if self.info.primary_value.metadata.unit == "kWh": - self._attr_device_class = DEVICE_CLASS_ENERGY @callback def async_update_last_reset( diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index c7100b22bd5..8c8a3f2e576 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -3,8 +3,10 @@ from datetime import datetime, timezone AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" -ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" +ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" +VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3" +CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4" SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index ebe406fb951..9fa4152ad6b 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -12,10 +12,14 @@ from homeassistant.components.zwave_js.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, @@ -25,6 +29,7 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, BASIC_SENSOR, + CURRENT_SENSOR, DATETIME_LAST_RESET, DATETIME_ZERO, ENERGY_SENSOR, @@ -34,6 +39,7 @@ from .common import ( METER_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, + VOLTAGE_SENSOR, ) @@ -72,6 +78,20 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT + state = hass.states.get(VOLTAGE_SENSOR) + + assert state + assert state.state == "122.96" + assert state.attributes["unit_of_measurement"] == ELECTRIC_POTENTIAL_VOLT + assert state.attributes["device_class"] == DEVICE_CLASS_VOLTAGE + + state = hass.states.get(CURRENT_SENSOR) + + assert state + assert state.state == "0.0" + assert state.attributes["unit_of_measurement"] == ELECTRIC_CURRENT_AMPERE + assert state.attributes["device_class"] == DEVICE_CLASS_CURRENT + async def test_disabled_notification_sensor(hass, multisensor_6, integration): """Test sensor is created from Notification CC and is disabled.""" From a671a0ccacce9991bf2bc5df8aad154105bcf7a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 23:26:39 +0200 Subject: [PATCH 746/818] Fix effect selector of light.turn_on service (#53726) --- homeassistant/components/light/services.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 778203a1c93..3b7df4e70c5 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -296,11 +296,7 @@ turn_on: name: Effect description: Light effect. selector: - select: - options: - - colorloop - - random - - white + text: turn_off: name: Turn off From 54eeebfd20d9f2ecfc6c381f4f81433caffe7942 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 14:26:05 -0700 Subject: [PATCH 747/818] Revert "Allow uploading large snapshots (#53528)" (#53729) This reverts commit cdce14d63db209acedb9888e726b813a069bf720. --- homeassistant/components/hassio/http.py | 54 ++++++++++++++++++------- tests/components/hassio/test_http.py | 49 ++++++---------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 73e5549be9a..302cc00bb9f 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,15 +1,16 @@ """HTTP Support for Hass.io.""" from __future__ import annotations +import asyncio import logging import os import re import aiohttp from aiohttp import web -from aiohttp.client import ClientError, ClientTimeout -from aiohttp.hdrs import CONTENT_TYPE +from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway +import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded @@ -19,6 +20,8 @@ from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) +MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 + NO_TIMEOUT = re.compile( r"^(?:" r"|homeassistant/update" @@ -72,28 +75,48 @@ class HassIOView(HomeAssistantView): async def _command_proxy( self, path: str, request: web.Request - ) -> web.StreamResponse: + ) -> web.Response | web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ + read_timeout = _get_timeout(path) + client_timeout = 10 + data = None headers = _init_header(request) if path in ("snapshots/new/upload", "backups/new/upload"): # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access + + # Backups are big, so we need to adjust the allowed size + request._client_max_size = ( # pylint: disable=protected-access + MAX_UPLOAD_SIZE + ) + client_timeout = 300 + try: - # Stream the request to the supervisor - client = await self._websession.request( - method=request.method, - url=f"http://{self._host}/{path}", + with async_timeout.timeout(client_timeout): + data = await request.read() + + method = getattr(self._websession, request.method.lower()) + client = await method( + f"http://{self._host}/{path}", + data=data, headers=headers, - data=request.content, - timeout=_get_timeout(path), + timeout=read_timeout, ) - # Stream the supervisor response back + # Simple request + if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: + # Return Response + body = await client.read() + return web.Response( + content_type=client.content_type, status=client.status, body=body + ) + + # Stream response response = web.StreamResponse(status=client.status, headers=client.headers) response.content_type = client.content_type @@ -103,9 +126,12 @@ class HassIOView(HomeAssistantView): return response - except ClientError as err: + except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) + except asyncio.TimeoutError: + _LOGGER.error("Client timeout error on API request %s", path) + raise HTTPBadGateway() @@ -125,11 +151,11 @@ def _init_header(request: web.Request) -> dict[str, str]: return headers -def _get_timeout(path: str) -> ClientTimeout: +def _get_timeout(path: str) -> int: """Return timeout for a URL path.""" if NO_TIMEOUT.match(path): - return ClientTimeout(connect=10) - return ClientTimeout(connect=10, total=300) + return 0 + return 300 def _need_auth(hass, path: str) -> bool: diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 881d3cc26ed..fc4bb3e6a0d 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,13 +1,11 @@ """The tests for the hassio component.""" -from aiohttp.client import ClientError -from aiohttp.streams import StreamReader -from aiohttp.test_utils import TestClient +import asyncio +from unittest.mock import patch + import pytest from homeassistant.components.hassio.http import _need_auth -from tests.test_util.aiohttp import AiohttpClientMocker - async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" @@ -108,6 +106,16 @@ async def test_forward_log_request(hassio_client, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 +async def test_bad_gateway_when_cannot_find_supervisor(hassio_client): + """Test we get a bad gateway error if we can't find supervisor.""" + with patch( + "homeassistant.components.hassio.http.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + resp = await hassio_client.get("/api/hassio/addons/test/info") + assert resp.status == 502 + + async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mock): """Test that we forward user info correctly.""" aioclient_mock.get("http://127.0.0.1/hello") @@ -163,37 +171,6 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): assert resp.headers["Content-Disposition"] == content_disposition -async def test_supervisor_client_error( - hassio_client: TestClient, aioclient_mock: AiohttpClientMocker -): - """Test any client error from the supervisor returns a 502.""" - # Create a request that throws a ClientError - async def raise_client_error(*args): - raise ClientError() - - aioclient_mock.get( - "http://127.0.0.1/test/raise/error", - side_effect=raise_client_error, - ) - - # Verify it returns bad gateway - resp = await hassio_client.get("/api/hassio/test/raise/error") - assert resp.status == 502 - assert len(aioclient_mock.mock_calls) == 1 - - -async def test_streamed_requests( - hassio_client: TestClient, aioclient_mock: AiohttpClientMocker -): - """Test requests get proxied to the supervisor as a stream.""" - aioclient_mock.get("http://127.0.0.1/test/stream") - await hassio_client.get("/api/hassio/test/stream", data="Test data") - assert len(aioclient_mock.mock_calls) == 1 - - # Verify the request body is passed as a StreamReader - assert isinstance(aioclient_mock.mock_calls[0][2], StreamReader) - - def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") From 630a1fb36ca39eb052f45c78a79974bce02950b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 14:27:55 -0700 Subject: [PATCH 748/818] Bumped version to 2021.8.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c7778e23c67..331b0056c4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 716c3f69ca6b6d26f243551bffa694b67a294463 Mon Sep 17 00:00:00 2001 From: Ryan Johnson <43426700+ryanjohnsontv@users.noreply.github.com> Date: Thu, 29 Jul 2021 23:19:32 -0500 Subject: [PATCH 749/818] Bump pyatv to 0.8.2 (#53659) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index d4eb322f4d7..a726e616641 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.8.1"], + "requirements": ["pyatv==0.8.2"], "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "after_dependencies": ["discovery"], "codeowners": ["@postlund"], diff --git a/requirements_all.txt b/requirements_all.txt index b7f8ec34eda..3b87599e997 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1326,7 +1326,7 @@ pyatmo==5.2.3 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.8.1 +pyatv==0.8.2 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8b1a136048..9ee06d81b00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -751,7 +751,7 @@ pyatag==0.3.5.3 pyatmo==5.2.3 # homeassistant.components.apple_tv -pyatv==0.8.1 +pyatv==0.8.2 # homeassistant.components.blackbird pyblackbird==0.5 From d54621e778701c4b15f573980f5540b458e8e8db Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Jul 2021 06:50:02 +0200 Subject: [PATCH 750/818] Extract smartthings switch energy attributes into sensors (#53719) --- .../components/smartthings/__init__.py | 3 +- .../components/smartthings/sensor.py | 263 ++++++++++++++---- .../components/smartthings/switch.py | 12 +- tests/components/smartthings/test_sensor.py | 46 ++- tests/components/smartthings/test_switch.py | 8 +- 5 files changed, 257 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 231cfa95263..bc64b173f20 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -9,6 +9,7 @@ import logging from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartthings import Attribute, Capability, SmartThings +from pysmartthings.device import DeviceEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -412,7 +413,7 @@ class DeviceBroker: class SmartThingsEntity(Entity): """Defines a SmartThings entity.""" - def __init__(self, device): + def __init__(self, device: DeviceEntity) -> None: """Initialize the instance.""" self._device = device self._dispatcher_remove = None diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index b8bb071fc0a..7a7f9a51855 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -3,18 +3,26 @@ from __future__ import annotations from collections import namedtuple from collections.abc import Sequence +from datetime import datetime from pysmartthings import Attribute, Capability +from pysmartthings.device import DeviceEntity -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, @@ -25,26 +33,27 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) +from homeassistant.util.dt import utc_from_timestamp from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN -Map = namedtuple("map", "attribute name default_unit device_class") +Map = namedtuple("map", "attribute name default_unit device_class state_class") CAPABILITY_TO_SENSORS = { Capability.activity_lighting_mode: [ - Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None) + Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None, None) ], Capability.air_conditioner_mode: [ - Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None) + Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None, None) ], Capability.air_quality_sensor: [ - Map(Attribute.air_quality, "Air Quality", "CAQI", None) + Map(Attribute.air_quality, "Air Quality", "CAQI", None, STATE_CLASS_MEASUREMENT) ], - Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None)], - Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None)], + Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None)], + Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None, None)], Capability.battery: [ - Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY) + Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY, None) ], Capability.body_mass_index_measurement: [ Map( @@ -52,57 +61,80 @@ CAPABILITY_TO_SENSORS = { "Body Mass Index", f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}", None, + STATE_CLASS_MEASUREMENT, ) ], Capability.body_weight_measurement: [ - Map(Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, None) + Map( + Attribute.body_weight_measurement, + "Body Weight", + MASS_KILOGRAMS, + None, + STATE_CLASS_MEASUREMENT, + ) ], Capability.carbon_dioxide_measurement: [ Map( Attribute.carbon_dioxide, "Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - None, + DEVICE_CLASS_CO2, + STATE_CLASS_MEASUREMENT, ) ], Capability.carbon_monoxide_detector: [ - Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None) + Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None, None) ], Capability.carbon_monoxide_measurement: [ Map( Attribute.carbon_monoxide_level, "Carbon Monoxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - None, + DEVICE_CLASS_CO, + STATE_CLASS_MEASUREMENT, ) ], Capability.dishwasher_operating_state: [ - Map(Attribute.machine_state, "Dishwasher Machine State", None, None), - Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None), + Map(Attribute.machine_state, "Dishwasher Machine State", None, None, None), + Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None, None), Map( Attribute.completion_time, "Dishwasher Completion Time", None, DEVICE_CLASS_TIMESTAMP, + None, ), ], - Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None)], + Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None, None)], Capability.dryer_operating_state: [ - Map(Attribute.machine_state, "Dryer Machine State", None, None), - Map(Attribute.dryer_job_state, "Dryer Job State", None, None), + Map(Attribute.machine_state, "Dryer Machine State", None, None, None), + Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None), Map( Attribute.completion_time, "Dryer Completion Time", None, DEVICE_CLASS_TIMESTAMP, + None, ), ], Capability.dust_sensor: [ - Map(Attribute.fine_dust_level, "Fine Dust Level", None, None), - Map(Attribute.dust_level, "Dust Level", None, None), + Map( + Attribute.fine_dust_level, + "Fine Dust Level", + None, + None, + STATE_CLASS_MEASUREMENT, + ), + Map(Attribute.dust_level, "Dust Level", None, None, STATE_CLASS_MEASUREMENT), ], Capability.energy_meter: [ - Map(Attribute.energy, "Energy Meter", ENERGY_KILO_WATT_HOUR, None) + Map( + Attribute.energy, + "Energy Meter", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + ) ], Capability.equivalent_carbon_dioxide_measurement: [ Map( @@ -110,6 +142,7 @@ CAPABILITY_TO_SENSORS = { "Equivalent Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, None, + STATE_CLASS_MEASUREMENT, ) ], Capability.formaldehyde_measurement: [ @@ -118,50 +151,94 @@ CAPABILITY_TO_SENSORS = { "Formaldehyde Measurement", CONCENTRATION_PARTS_PER_MILLION, None, + STATE_CLASS_MEASUREMENT, ) ], Capability.gas_meter: [ - Map(Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, None), - Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None), - Map(Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP), - Map(Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, None), + Map( + Attribute.gas_meter, + "Gas Meter", + ENERGY_KILO_WATT_HOUR, + None, + STATE_CLASS_MEASUREMENT, + ), + Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None), + Map( + Attribute.gas_meter_time, + "Gas Meter Time", + None, + DEVICE_CLASS_TIMESTAMP, + None, + ), + Map( + Attribute.gas_meter_volume, + "Gas Meter Volume", + VOLUME_CUBIC_METERS, + None, + STATE_CLASS_MEASUREMENT, + ), ], Capability.illuminance_measurement: [ - Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE) + Map( + Attribute.illuminance, + "Illuminance", + LIGHT_LUX, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + ) ], Capability.infrared_level: [ - Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None) + Map( + Attribute.infrared_level, + "Infrared Level", + PERCENTAGE, + None, + STATE_CLASS_MEASUREMENT, + ) ], Capability.media_input_source: [ - Map(Attribute.input_source, "Media Input Source", None, None) + Map(Attribute.input_source, "Media Input Source", None, None, None) ], Capability.media_playback_repeat: [ - Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None) + Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None, None) ], Capability.media_playback_shuffle: [ - Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None) + Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None) ], Capability.media_playback: [ - Map(Attribute.playback_status, "Media Playback Status", None, None) + Map(Attribute.playback_status, "Media Playback Status", None, None, None) ], - Capability.odor_sensor: [Map(Attribute.odor_level, "Odor Sensor", None, None)], - Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None)], + Capability.odor_sensor: [ + Map(Attribute.odor_level, "Odor Sensor", None, None, None) + ], + Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None, None)], Capability.oven_operating_state: [ - Map(Attribute.machine_state, "Oven Machine State", None, None), - Map(Attribute.oven_job_state, "Oven Job State", None, None), - Map(Attribute.completion_time, "Oven Completion Time", None, None), + Map(Attribute.machine_state, "Oven Machine State", None, None, None), + Map(Attribute.oven_job_state, "Oven Job State", None, None, None), + Map(Attribute.completion_time, "Oven Completion Time", None, None, None), ], Capability.oven_setpoint: [ - Map(Attribute.oven_setpoint, "Oven Set Point", None, None) + Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None) + ], + Capability.power_meter: [ + Map( + Attribute.power, + "Power Meter", + POWER_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ) + ], + Capability.power_source: [ + Map(Attribute.power_source, "Power Source", None, None, None) ], - Capability.power_meter: [Map(Attribute.power, "Power Meter", POWER_WATT, None)], - Capability.power_source: [Map(Attribute.power_source, "Power Source", None, None)], Capability.refrigeration_setpoint: [ Map( Attribute.refrigeration_setpoint, "Refrigeration Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.relative_humidity_measurement: [ @@ -170,6 +247,7 @@ CAPABILITY_TO_SENSORS = { "Relative Humidity Measurement", PERCENTAGE, DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, ) ], Capability.robot_cleaner_cleaning_mode: [ @@ -178,25 +256,43 @@ CAPABILITY_TO_SENSORS = { "Robot Cleaner Cleaning Mode", None, None, + None, ) ], Capability.robot_cleaner_movement: [ - Map(Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None) + Map( + Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None, None + ) ], Capability.robot_cleaner_turbo_mode: [ - Map(Attribute.robot_cleaner_turbo_mode, "Robot Cleaner Turbo Mode", None, None) + Map( + Attribute.robot_cleaner_turbo_mode, + "Robot Cleaner Turbo Mode", + None, + None, + None, + ) ], Capability.signal_strength: [ - Map(Attribute.lqi, "LQI Signal Strength", None, None), - Map(Attribute.rssi, "RSSI Signal Strength", None, None), + Map(Attribute.lqi, "LQI Signal Strength", None, None, STATE_CLASS_MEASUREMENT), + Map( + Attribute.rssi, + "RSSI Signal Strength", + None, + DEVICE_CLASS_SIGNAL_STRENGTH, + STATE_CLASS_MEASUREMENT, + ), + ], + Capability.smoke_detector: [ + Map(Attribute.smoke, "Smoke Detector", None, None, None) ], - Capability.smoke_detector: [Map(Attribute.smoke, "Smoke Detector", None, None)], Capability.temperature_measurement: [ Map( Attribute.temperature, "Temperature Measurement", None, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, ) ], Capability.thermostat_cooling_setpoint: [ @@ -205,10 +301,11 @@ CAPABILITY_TO_SENSORS = { "Thermostat Cooling Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.thermostat_fan_mode: [ - Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None) + Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None, None) ], Capability.thermostat_heating_setpoint: [ Map( @@ -216,10 +313,11 @@ CAPABILITY_TO_SENSORS = { "Thermostat Heating Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.thermostat_mode: [ - Map(Attribute.thermostat_mode, "Thermostat Mode", None, None) + Map(Attribute.thermostat_mode, "Thermostat Mode", None, None, None) ], Capability.thermostat_operating_state: [ Map( @@ -227,6 +325,7 @@ CAPABILITY_TO_SENSORS = { "Thermostat Operating State", None, None, + None, ) ], Capability.thermostat_setpoint: [ @@ -235,12 +334,13 @@ CAPABILITY_TO_SENSORS = { "Thermostat Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.three_axis: [], Capability.tv_channel: [ - Map(Attribute.tv_channel, "Tv Channel", None, None), - Map(Attribute.tv_channel_name, "Tv Channel Name", None, None), + Map(Attribute.tv_channel, "Tv Channel", None, None, None), + Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None), ], Capability.tvoc_measurement: [ Map( @@ -248,23 +348,39 @@ CAPABILITY_TO_SENSORS = { "Tvoc Measurement", CONCENTRATION_PARTS_PER_MILLION, None, + STATE_CLASS_MEASUREMENT, ) ], Capability.ultraviolet_index: [ - Map(Attribute.ultraviolet_index, "Ultraviolet Index", None, None) + Map( + Attribute.ultraviolet_index, + "Ultraviolet Index", + None, + None, + STATE_CLASS_MEASUREMENT, + ) ], Capability.voltage_measurement: [ - Map(Attribute.voltage, "Voltage Measurement", ELECTRIC_POTENTIAL_VOLT, None) + Map( + Attribute.voltage, + "Voltage Measurement", + ELECTRIC_POTENTIAL_VOLT, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ) + ], + Capability.washer_mode: [ + Map(Attribute.washer_mode, "Washer Mode", None, None, None) ], - Capability.washer_mode: [Map(Attribute.washer_mode, "Washer Mode", None, None)], Capability.washer_operating_state: [ - Map(Attribute.machine_state, "Washer Machine State", None, None), - Map(Attribute.washer_job_state, "Washer Job State", None, None), + Map(Attribute.machine_state, "Washer Machine State", None, None, None), + Map(Attribute.washer_job_state, "Washer Job State", None, None, None), Map( Attribute.completion_time, "Washer Completion Time", None, DEVICE_CLASS_TIMESTAMP, + None, ), ], } @@ -292,11 +408,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors.extend( [ SmartThingsSensor( - device, m.attribute, m.name, m.default_unit, m.device_class + device, + m.attribute, + m.name, + m.default_unit, + m.device_class, + m.state_class, ) for m in maps ] ) + + if broker.any_assigned(device.device_id, "switch"): + for capability in (Capability.energy_meter, Capability.power_meter): + maps = CAPABILITY_TO_SENSORS[capability] + sensors.extend( + [ + SmartThingsSensor( + device, + m.attribute, + m.name, + m.default_unit, + m.device_class, + m.state_class, + ) + for m in maps + ] + ) + async_add_entities(sensors) @@ -311,14 +450,21 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Sensor.""" def __init__( - self, device, attribute: str, name: str, default_unit: str, device_class: str - ): + self, + device: DeviceEntity, + attribute: str, + name: str, + default_unit: str, + device_class: str, + state_class: str | None, + ) -> None: """Init the class.""" super().__init__(device) self._attribute = attribute self._name = name self._device_class = device_class self._default_unit = default_unit + self._attr_state_class = state_class @property def name(self) -> str: @@ -346,6 +492,13 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): unit = self._device.status.attributes[self._attribute].unit return UNITS.get(unit, unit) if unit else self._default_unit + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + if self._attribute == Attribute.energy: + return utc_from_timestamp(0) + return None + class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Three Axis Sensor.""" diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 7b8364d9ba3..7e92ba4f663 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from pysmartthings import Attribute, Capability +from pysmartthings import Capability from homeassistant.components.switch import SwitchEntity @@ -48,16 +48,6 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self._device.status.attributes[Attribute.power].value - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self._device.status.attributes[Attribute.energy].value - @property def is_on(self) -> bool: """Return true if light is on.""" diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 4af88e27fe4..ffb577c903a 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -6,7 +6,11 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability -from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASSES, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASSES, +) from homeassistant.components.smartthings import sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.config_entries import ConfigEntryState @@ -33,6 +37,8 @@ async def test_mapping_integrity(): assert ( sensor_map.device_class in DEVICE_CLASSES ), sensor_map.device_class + if sensor_map.state_class: + assert sensor_map.state_class in STATE_CLASSES, sensor_map.state_class async def test_entity_state(hass, device_factory): @@ -95,6 +101,44 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry.manufacturer == "Unavailable" +async def test_energy_sensors_for_switch_device(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory( + "Switch_1", + [Capability.switch, Capability.power_meter, Capability.energy_meter], + {Attribute.switch: "off", Attribute.power: 355, Attribute.energy: 11.422}, + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + # Act + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + # Assert + state = hass.states.get("sensor.switch_1_energy_meter") + assert state + assert state.state == "11.422" + entry = entity_registry.async_get("sensor.switch_1_energy_meter") + assert entry + assert entry.unique_id == f"{device.device_id}.{Attribute.energy}" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + state = hass.states.get("sensor.switch_1_power_meter") + assert state + assert state.state == "355" + entry = entity_registry.async_get("sensor.switch_1_power_meter") + assert entry + assert entry.unique_id == f"{device.device_id}.{Attribute.power}" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + async def test_update_from_signal(hass, device_factory): """Test the binary_sensor updates when receiving a signal.""" # Arrange diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 7c202fad12e..c884d601baf 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -7,11 +7,7 @@ real HTTP calls are not initiated during testing. from pysmartthings import Attribute, Capability from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - DOMAIN as SWITCH_DOMAIN, -) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -72,8 +68,6 @@ async def test_turn_on(hass, device_factory): state = hass.states.get("switch.switch_1") assert state is not None assert state.state == "on" - assert state.attributes[ATTR_CURRENT_POWER_W] == 355 - assert state.attributes[ATTR_TODAY_ENERGY_KWH] == 11.422 async def test_update_from_signal(hass, device_factory): From bfacff5d78186d7e1c41ced62dbf4f5f1950cf93 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Jul 2021 01:16:08 +0200 Subject: [PATCH 751/818] Add energy device class to deCONZ consumption sensors (#53731) --- homeassistant/components/deconz/sensor.py | 7 +++++++ tests/components/deconz/test_sensor.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index e0f12303946..9282f2d26cc 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -40,6 +41,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.util import dt as dt_util from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -51,6 +53,7 @@ ATTR_DAYLIGHT = "daylight" ATTR_EVENT_ID = "event_id" DEVICE_CLASS = { + Consumption: DEVICE_CLASS_ENERGY, Humidity: DEVICE_CLASS_HUMIDITY, LightLevel: DEVICE_CLASS_ILLUMINANCE, Power: DEVICE_CLASS_POWER, @@ -65,6 +68,7 @@ ICON = { } STATE_CLASS = { + Consumption: STATE_CLASS_MEASUREMENT, Humidity: STATE_CLASS_MEASUREMENT, Pressure: STATE_CLASS_MEASUREMENT, Temperature: STATE_CLASS_MEASUREMENT, @@ -158,6 +162,9 @@ class DeconzSensor(DeconzDevice, SensorEntity): self._attr_state_class = STATE_CLASS.get(type(self._device)) self._attr_unit_of_measurement = UNIT_OF_MEASUREMENT.get(type(self._device)) + if device.type in Consumption.ZHATYPE: + self._attr_last_reset = dt_util.utc_from_timestamp(0) + @callback def async_update_callback(self, force_update=False): """Update the sensor's state.""" diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index d9c4adf1388..7f8bce24d80 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, @@ -118,7 +119,7 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): consumption_sensor = hass.states.get("sensor.consumption_sensor") assert consumption_sensor.state == "0.002" - assert ATTR_DEVICE_CLASS not in consumption_sensor.attributes + assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY assert not hass.states.get("sensor.clip_light_level_sensor") From 37c3062874bef5a2c38c9a06b9252599043979e9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 29 Jul 2021 19:16:47 -0400 Subject: [PATCH 752/818] Bump up ZHA dependencies (#53732) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c8df2ddeb30..fa117a3f1ff 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,7 +10,7 @@ "zha-quirks==0.0.59", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.36.0", + "zigpy==0.36.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.2" diff --git a/requirements_all.txt b/requirements_all.txt index 3b87599e997..75313a53947 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.2 # homeassistant.components.zha -zigpy==0.36.0 +zigpy==0.36.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ee06d81b00..7a49e3faae2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1362,7 +1362,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.2 # homeassistant.components.zha -zigpy==0.36.0 +zigpy==0.36.1 # homeassistant.components.zwave_js zwave-js-server-python==0.28.0 From 0442827b9ecf0cf169182e8a0501f8312127237a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Jul 2021 06:18:09 +0200 Subject: [PATCH 753/818] Fix exception handling in DataUpdateCoordinator in TP-Link (#53734) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tplink/__init__.py | 81 +++++++++++---------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 9d5263687f8..e309d2c5082 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.dt import utc_from_timestamp from .common import SmartDevices, async_discover_devices, get_static_devices @@ -185,45 +185,50 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict: """Fetch all device and sensor data from api.""" - info = self.smartplug.sys_info - data = { - CONF_HOST: self.smartplug.host, - CONF_MAC: info["mac"], - CONF_MODEL: info["model"], - CONF_SW_VERSION: info["sw_ver"], - } - if self.smartplug.context is None: - data[CONF_ALIAS] = info["alias"] - data[CONF_DEVICE_ID] = info["mac"] - data[CONF_STATE] = self.smartplug.state == self.smartplug.SWITCH_STATE_ON - else: - plug_from_context = next( - c - for c in self.smartplug.sys_info["children"] - if c["id"] == self.smartplug.context - ) - data[CONF_ALIAS] = plug_from_context["alias"] - data[CONF_DEVICE_ID] = self.smartplug.context - data[CONF_STATE] = plug_from_context["state"] == 1 - if self.smartplug.has_emeter: - emeter_readings = self.smartplug.get_emeter_realtime() - data[CONF_EMETER_PARAMS] = { - ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), - ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), - ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), - ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), - ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, + try: + info = self.smartplug.sys_info + data = { + CONF_HOST: self.smartplug.host, + CONF_MAC: info["mac"], + CONF_MODEL: info["model"], + CONF_SW_VERSION: info["sw_ver"], } - emeter_statics = self.smartplug.get_emeter_daily() - data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TODAY_ENERGY_KWH - ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - if emeter_statics.get(int(time.strftime("%e"))): - data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( - float(emeter_statics[int(time.strftime("%e"))]), 3 + if self.smartplug.context is None: + data[CONF_ALIAS] = info["alias"] + data[CONF_DEVICE_ID] = info["mac"] + data[CONF_STATE] = ( + self.smartplug.state == self.smartplug.SWITCH_STATE_ON ) else: - # today's consumption not available, when device was off all the day - data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0 + plug_from_context = next( + c + for c in self.smartplug.sys_info["children"] + if c["id"] == self.smartplug.context + ) + data[CONF_ALIAS] = plug_from_context["alias"] + data[CONF_DEVICE_ID] = self.smartplug.context + data[CONF_STATE] = plug_from_context["state"] == 1 + if self.smartplug.has_emeter: + emeter_readings = self.smartplug.get_emeter_realtime() + data[CONF_EMETER_PARAMS] = { + ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), + ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), + ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), + ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), + ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, + } + emeter_statics = self.smartplug.get_emeter_daily() + data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TODAY_ENERGY_KWH + ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + if emeter_statics.get(int(time.strftime("%e"))): + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( + float(emeter_statics[int(time.strftime("%e"))]), 3 + ) + else: + # today's consumption not available, when device was off all the day + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0 + except SmartDeviceException as ex: + raise UpdateFailed(ex) from ex return data From d2dfdd81adff1ec13cade4cb1c472e9f190ec208 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 30 Jul 2021 01:08:52 -0400 Subject: [PATCH 754/818] Only allow one Mazda vehicle status request at a time (#53736) --- homeassistant/components/mazda/__init__.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 921dd4c06c5..c64a3b35993 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -28,7 +28,6 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, UpdateFailed, ) -from homeassistant.util.async_ import gather_with_concurrency from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICES @@ -143,14 +142,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: vehicles = await with_timeout(mazda_client.get_vehicles()) - vehicle_status_tasks = [ - with_timeout(mazda_client.get_vehicle_status(vehicle["id"])) - for vehicle in vehicles - ] - statuses = await gather_with_concurrency(5, *vehicle_status_tasks) - - for vehicle, status in zip(vehicles, statuses): - vehicle["status"] = status + # The Mazda API can throw an error when multiple simultaneous requests are + # made for the same account, so we can only make one request at a time here + for vehicle in vehicles: + vehicle["status"] = await with_timeout( + mazda_client.get_vehicle_status(vehicle["id"]) + ) hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles From 128dc07fa5388852cdd60730dcd86249a674cfaa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Jul 2021 07:11:15 +0200 Subject: [PATCH 755/818] Apply left suggestions #53596 for TP-Link (#53737) --- homeassistant/components/tplink/const.py | 55 ------------- homeassistant/components/tplink/sensor.py | 62 ++++++++++++++- homeassistant/components/tplink/switch.py | 4 +- tests/components/tplink/test_init.py | 95 +++++++---------------- 4 files changed, 91 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 93cad889a2f..888d671096d 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -3,23 +3,6 @@ from __future__ import annotations import datetime -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - SensorEntityDescription, -) -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH -from homeassistant.const import ( - ATTR_VOLTAGE, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) - DOMAIN = "tplink" COORDINATORS = "coordinators" @@ -41,41 +24,3 @@ CONF_SWITCH = "switch" CONF_SENSOR = "sensor" PLATFORMS = [CONF_LIGHT, CONF_SENSOR, CONF_SWITCH] - -ENERGY_SENSORS: list[SensorEntityDescription] = [ - SensorEntityDescription( - key=ATTR_CURRENT_POWER_W, - unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - name="Current Consumption", - ), - SensorEntityDescription( - key=ATTR_TOTAL_ENERGY_KWH, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - name="Total Consumption", - ), - SensorEntityDescription( - key=ATTR_TODAY_ENERGY_KWH, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - name="Today's Consumption", - ), - SensorEntityDescription( - key=ATTR_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - name="Voltage", - ), - SensorEntityDescription( - key=ATTR_CURRENT_A, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - name="Current", - ), -] diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 24b93e90963..bb6596b82d1 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -1,18 +1,32 @@ """Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" from __future__ import annotations -from typing import Any +from typing import Any, Final from pyHS100 import SmartPlug from homeassistant.components.sensor import ( ATTR_LAST_RESET, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ALIAS, + CONF_DEVICE_ID, + CONF_MAC, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -29,9 +43,51 @@ from .const import ( CONF_SWITCH, COORDINATORS, DOMAIN as TPLINK_DOMAIN, - ENERGY_SENSORS, ) +ATTR_CURRENT_A = "current_a" +ATTR_CURRENT_POWER_W = "current_power_w" +ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" +ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" + +ENERGY_SENSORS: Final[list[SensorEntityDescription]] = [ + SensorEntityDescription( + key=ATTR_CURRENT_POWER_W, + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + name="Current Consumption", + ), + SensorEntityDescription( + key=ATTR_TOTAL_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Total Consumption", + ), + SensorEntityDescription( + key=ATTR_TODAY_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Today's Consumption", + ), + SensorEntityDescription( + key=ATTR_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + name="Voltage", + ), + SensorEntityDescription( + key=ATTR_CURRENT_A, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + name="Current", + ), +] + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 688091991c3..f5319de999a 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -88,10 +88,10 @@ class SmartPlugSwitch(CoordinatorEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.hass.async_add_job(self.smartplug.turn_on) + await self.hass.async_add_executor_job(self.smartplug.turn_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.hass.async_add_job(self.smartplug.turn_off) + await self.hass.async_add_executor_job(self.smartplug.turn_off) await self.coordinator.async_refresh() diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 0cfb4d3d233..fb3f44709fc 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,7 +1,6 @@ """Tests for the TP-Link component.""" from __future__ import annotations -from datetime import datetime import time from typing import Any from unittest.mock import MagicMock, patch @@ -12,31 +11,21 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink -from homeassistant.components.sensor import ATTR_LAST_RESET -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH from homeassistant.components.tplink.common import SmartDevices from homeassistant.components.tplink.const import ( - ATTR_CURRENT_A, - ATTR_TOTAL_ENERGY_KWH, CONF_DIMMER, CONF_DISCOVERY, - CONF_EMETER_PARAMS, CONF_LIGHT, - CONF_MODEL, CONF_SW_VERSION, CONF_SWITCH, COORDINATORS, ) -from homeassistant.const import ( - ATTR_VOLTAGE, - CONF_ALIAS, - CONF_DEVICE_ID, - CONF_HOST, - CONF_MAC, -) +from homeassistant.components.tplink.sensor import ENERGY_SENSORS +from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util import slugify from tests.common import MockConfigEntry, mock_coro from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA @@ -217,20 +206,16 @@ async def test_platforms_are_initialized(hass: HomeAssistant): } } - with patch( - "homeassistant.components.tplink.common.Discover.discover" - ) as discover, patch( + with patch("homeassistant.components.tplink.common.Discover.discover"), patch( "homeassistant.components.tplink.get_static_devices" ) as get_static_devices, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ) as light_setup, patch( - "homeassistant.components.tplink.switch.async_setup_entry", - return_value=mock_coro(True), - ) as switch_setup, patch( - "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", + False, ): light = SmartBulb("123.123.123.123") @@ -248,47 +233,25 @@ async def test_platforms_are_initialized(hass: HomeAssistant): await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert hass.data.get(tplink.DOMAIN) - assert hass.data[tplink.DOMAIN].get(COORDINATORS) - assert hass.data[tplink.DOMAIN][COORDINATORS].get(switch.mac) - assert isinstance( - hass.data[tplink.DOMAIN][COORDINATORS][switch.mac], - tplink.SmartPlugDataUpdateCoordinator, - ) - data = hass.data[tplink.DOMAIN][COORDINATORS][switch.mac].data - assert data[CONF_HOST] == switch.host - assert data[CONF_MAC] == switch.sys_info["mac"] - assert data[CONF_MODEL] == switch.sys_info["model"] - assert data[CONF_SW_VERSION] == switch.sys_info["sw_ver"] - assert data[CONF_ALIAS] == switch.sys_info["alias"] - assert data[CONF_DEVICE_ID] == switch.sys_info["mac"] + state = hass.states.get(f"switch.{switch.alias}") + assert state + assert state.name == switch.alias - emeter_readings = switch.get_emeter_realtime() - assert data[CONF_EMETER_PARAMS][ATTR_VOLTAGE] == round( - float(emeter_readings["voltage"]), 1 - ) - assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_A] == round( - float(emeter_readings["current"]), 2 - ) - assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_POWER_W] == round( - float(emeter_readings["power"]), 2 - ) - assert data[CONF_EMETER_PARAMS][ATTR_TOTAL_ENERGY_KWH] == round( - float(emeter_readings["total"]), 3 - ) - assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TOTAL_ENERGY_KWH - ] == utc_from_timestamp(0) + for description in ENERGY_SENSORS: + state = hass.states.get( + f"sensor.{switch.alias}_{slugify(description.name)}" + ) + assert state + assert state.state is not None + assert state.name == f"{switch.alias} {description.name}" - assert data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] == 1.123 - assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TODAY_ENERGY_KWH - ] == datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - - assert discover.call_count == 0 - assert get_static_devices.call_count == 1 - assert light_setup.call_count == 1 - assert switch_setup.call_count == 1 + device_registry = dr.async_get(hass) + assert len(device_registry.devices) == 1 + device = next(iter(device_registry.devices.values())) + assert device.name == switch.alias + assert device.model == switch.model + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, switch.mac.lower())} + assert device.sw_version == switch.sys_info[CONF_SW_VERSION] async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): @@ -311,8 +274,6 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): "homeassistant.components.tplink.switch.async_setup_entry", return_value=mock_coro(True), ), patch( - "homeassistant.components.tplink.sensor.SmartPlugSensor.__init__" - ) as SmartPlugSensor, patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): @@ -323,7 +284,11 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert SmartPlugSensor.call_count == 0 + for description in ENERGY_SENSORS: + state = hass.states.get( + f"sensor.{switch.alias}_{slugify(description.name)}" + ) + assert state is None async def test_smartstrip_device(hass: HomeAssistant): From 83e4e4f769f810939cfbed2eda30223236ba373e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Jul 2021 07:10:16 +0200 Subject: [PATCH 756/818] Fix Xiaomi humidifier name migration (#53738) --- homeassistant/components/xiaomi_miio/__init__.py | 6 ++---- homeassistant/components/xiaomi_miio/const.py | 1 - homeassistant/components/xiaomi_miio/humidifier.py | 6 +----- homeassistant/components/xiaomi_miio/number.py | 7 +------ homeassistant/components/xiaomi_miio/select.py | 7 +------ homeassistant/components/xiaomi_miio/sensor.py | 9 ++------- homeassistant/components/xiaomi_miio/switch.py | 7 +------ 7 files changed, 8 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 4ca64fc175c..e858c7ee797 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -21,7 +21,6 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODELS_AIR_MONITOR, MODELS_FAN, MODELS_HUMIDIFIER, @@ -112,12 +111,13 @@ async def async_create_miio_device_and_coordinator( else: device = AirHumidifier(host, token, model=model) - # Removing fan platform entity for humidifiers and cache the name and entity name for migration + # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration entity_registry = er.async_get(hass) entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future migrate_entity_name = entity_registry.async_get(entity_id).name + hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) async def async_update_data(): @@ -142,8 +142,6 @@ async def async_create_miio_device_and_coordinator( KEY_DEVICE: device, KEY_COORDINATOR: coordinator, } - if migrate_entity_name: - hass.data[DOMAIN][entry.entry_id][KEY_MIGRATE_ENTITY_NAME] = migrate_entity_name # Trigger first data fetch await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 05499efb5d3..0d8d5bc0014 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -18,7 +18,6 @@ CONF_CLOUD_SUBDEVICES = "cloud_subdevices" # Keys KEY_COORDINATOR = "coordinator" KEY_DEVICE = "device" -KEY_MIGRATE_ENTITY_NAME = "migrate_entity_name" # Attributes ATTR_AVAILABLE = "available" diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 9d9f49229d1..eb45a716254 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -24,7 +24,6 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -52,10 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title + name = config_entry.title if model in MODELS_HUMIDIFIER_MIOT: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 4d0e92b104f..6855faa6391 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -13,7 +13,6 @@ from .const import ( FEATURE_SET_MOTOR_SPEED, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA4, ) from .device import XiaomiCoordinatedMiioEntity @@ -58,10 +57,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title if model not in [MODEL_AIRHUMIDIFIER_CA4]: return @@ -69,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for number in NUMBER_TYPES.values(): entities.append( XiaomiAirHumidifierNumber( - f"{name} {number.name}", + f"{config_entry.title} {number.name}", device, config_entry, f"{number.short_name}_{config_entry.unique_id}", diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 055d8073739..77aba961244 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -16,7 +16,6 @@ from .const import ( FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -67,10 +66,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: entity_class = XiaomiAirHumidifierSelector @@ -84,7 +79,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for selector in SELECTOR_TYPES.values(): entities.append( entity_class( - f"{name} {selector.name}", + f"{config_entry.title} {selector.name}", device, config_entry, f"{selector.short_name}_{config_entry.unique_id}", diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 3c28d8496e7..413971aa880 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -45,7 +45,6 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODELS_HUMIDIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity @@ -190,11 +189,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = None sensors = [] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title - if model in MODELS_HUMIDIFIER_MIOT: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -205,6 +199,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = HUMIDIFIER_SENSORS else: unique_id = config_entry.unique_id + name = config_entry.title _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) device = AirQualityMonitor(host, token) @@ -214,7 +209,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: entities.append( XiaomiGenericSensor( - f"{name} {sensor.replace('_', ' ').title()}", + f"{config_entry.title} {sensor.replace('_', ' ').title()}", device, config_entry, f"{sensor}_{config_entry.unique_id}", diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 35ac4c7da4f..c86c98de34c 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -41,7 +41,6 @@ from .const import ( FEATURE_SET_DRY, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -235,10 +234,6 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): unique_id = config_entry.unique_id device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -256,7 +251,7 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): if feature & device_features: entities.append( XiaomiGenericCoordinatedSwitch( - f"{name} {switch.name}", + f"{config_entry.title} {switch.name}", device, config_entry, f"{switch.short_name}_{unique_id}", From d34bd8ad1e43f548d4908c140ef0ef73a1834136 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Jul 2021 07:10:41 +0200 Subject: [PATCH 757/818] Fix Xiaomi-miio switch platform setup (#53739) --- homeassistant/components/xiaomi_miio/switch.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index c86c98de34c..bdf3085f236 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -218,13 +218,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the switch from a config entry.""" - if ( - config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY - or config_entry.data[CONF_MODEL] == "lumi.acpartner.v3" - ): - await async_setup_other_entry(hass, config_entry, async_add_entities) - else: + if config_entry.data[CONF_MODEL] in MODELS_HUMIDIFIER: await async_setup_coordinated_entry(hass, config_entry, async_add_entities) + else: + await async_setup_other_entry(hass, config_entry, async_add_entities) async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): From 9dcd3f662693d00d769dd95ec2af3c9bf3ade214 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 23:34:03 -0700 Subject: [PATCH 758/818] Add energy attributes to Fronius (#53741) * Add energy attributes to Fronius * Add solar * Add power * Only add last reset for total meter entities * Only add last reset for total solar entities * Create different entity descriptions per key * only return the entity description for energy total * Use correct key * Meter devices keep it real * keys start with energy_real * Also device key starts with * Lint --- homeassistant/components/fronius/sensor.py | 117 ++++++++++++++++----- 1 file changed, 88 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ac006638912..6f949334d02 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -8,17 +8,26 @@ import logging from pyfronius import Fronius import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_DEVICE, CONF_MONITORED_CONDITIONS, CONF_RESOURCE, CONF_SCAN_INTERVAL, CONF_SENSOR_TYPE, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, ) +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -152,6 +161,12 @@ class FroniusAdapter: """Whether the fronius device is active.""" return self._available + def entity_description( # pylint: disable=no-self-use + self, key + ) -> SensorEntityDescription | None: + """Create entity description for a key.""" + return None + async def async_update(self): """Retrieve and update latest state.""" try: @@ -198,14 +213,28 @@ class FroniusAdapter: async def _update(self) -> dict: """Return values of interest.""" - async def register(self, sensor): + @callback + def register(self, sensor): """Register child sensor for update subscriptions.""" self._registered_sensors.add(sensor) + return lambda: self._registered_sensors.remove(sensor) class FroniusInverterSystem(FroniusAdapter): """Adapter for the fronius inverter with system scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if key != "energy_total": + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_system_inverter_data() @@ -214,6 +243,18 @@ class FroniusInverterSystem(FroniusAdapter): class FroniusInverterDevice(FroniusAdapter): """Adapter for the fronius inverter with device scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if key != "energy_total": + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_inverter_data(self._device) @@ -230,6 +271,18 @@ class FroniusStorage(FroniusAdapter): class FroniusMeterSystem(FroniusAdapter): """Adapter for the fronius meter with system scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if not key.startswith("energy_real_"): + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_system_meter_data() @@ -238,6 +291,18 @@ class FroniusMeterSystem(FroniusAdapter): class FroniusMeterDevice(FroniusAdapter): """Adapter for the fronius meter with device scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if not key.startswith("energy_real_"): + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_meter_data(self._device) @@ -246,6 +311,14 @@ class FroniusMeterDevice(FroniusAdapter): class FroniusPowerFlow(FroniusAdapter): """Adapter for the fronius power flow.""" + def entity_description(self, key): + """Return the entity descriptor.""" + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_power_flow() @@ -254,27 +327,13 @@ class FroniusPowerFlow(FroniusAdapter): class FroniusTemplateSensor(SensorEntity): """Sensor for the single values (e.g. pv power, ac power).""" - def __init__(self, parent: FroniusAdapter, name): + def __init__(self, parent: FroniusAdapter, key): """Initialize a singular value sensor.""" - self._name = name - self.parent = parent - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name.replace('_', ' ').capitalize()} {self.parent.name}" - - @property - def state(self): - """Return the current state.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit + self._key = key + self._attr_name = f"{key.replace('_', ' ').capitalize()} {parent.name}" + self._parent = parent + if entity_description := parent.entity_description(key): + self.entity_description = entity_description @property def should_poll(self): @@ -284,19 +343,19 @@ class FroniusTemplateSensor(SensorEntity): @property def available(self): """Whether the fronius device is active.""" - return self.parent.available + return self._parent.available async def async_update(self): """Update the internal state.""" - state = self.parent.data.get(self._name) - self._state = state.get("value") - if isinstance(self._state, float): - self._state = round(self._state, 2) - self._unit = state.get("unit") + state = self._parent.data.get(self._key) + self._attr_state = state.get("value") + if isinstance(self._attr_state, float): + self._attr_state = round(self._attr_state, 2) + self._attr_unit_of_measurement = state.get("unit") async def async_added_to_hass(self): """Register at parent component for updates.""" - await self.parent.register(self) + self.async_on_remove(self._parent.register(self)) def __hash__(self): """Hash sensor by hashing its name.""" From 447901c22370b7ae2d41932d2648db29d69bc038 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 23:35:09 -0700 Subject: [PATCH 759/818] Bumped version to 2021.8.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 331b0056c4e..bc79728c307 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From dac968bf32b3991357e21c85b4b29a2689ab1c98 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 30 Jul 2021 23:02:33 +0800 Subject: [PATCH 760/818] Fix non monotonic dts error in stream (#53712) * Use defaultdict for TimestampValidator._last_dts * Combine filters * Allow PeekIterator to be updated while preserving buffer * Fix peek edge case * Readd is_valid filter to video only iterator --- homeassistant/components/stream/worker.py | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 72625fa0f5a..69def43b2a2 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -1,7 +1,7 @@ """Provides the worker thread needed for processing streams.""" from __future__ import annotations -from collections import deque +from collections import defaultdict, deque from collections.abc import Generator, Iterator, Mapping from io import BytesIO import logging @@ -222,6 +222,12 @@ class PeekIterator(Iterator): """Return and consume the next item available.""" return self._next() + def replace_underlying_iterator(self, new_iterator: Iterator) -> None: + """Replace the underlying iterator while preserving the buffer.""" + self._iterator = new_iterator + if self._next is not self._pop_buffer: + self._next = self._iterator.__next__ + def _pop_buffer(self) -> av.Packet: """Consume items from the buffer until exhausted.""" if self._buffer: @@ -248,7 +254,9 @@ class TimestampValidator: def __init__(self) -> None: """Initialize the TimestampValidator.""" # Decompression timestamp of last packet in each stream - self._last_dts: dict[av.stream.Stream, float] = {} + self._last_dts: dict[av.stream.Stream, int | float] = defaultdict( + lambda: float("-inf") + ) # Number of consecutive missing decompression timestamps self._missing_dts = 0 @@ -264,7 +272,7 @@ class TimestampValidator: return False self._missing_dts = 0 # Discard when dts is not monotonic. Terminate if gap is too wide. - prev_dts = self._last_dts.get(packet.stream, float("-inf")) + prev_dts = self._last_dts[packet.stream] if packet.dts <= prev_dts: gap = packet.time_base * (prev_dts - packet.dts) if gap > MAX_TIMESTAMP_GAP: @@ -350,19 +358,25 @@ def stream_worker( try: if audio_stream and unsupported_audio(container_packets.peek(), audio_stream): audio_stream = None - container_packets = PeekIterator( + container_packets.replace_underlying_iterator( filter(dts_validator.is_valid, container.demux(video_stream)) ) # Advance to the first keyframe for muxing, then rewind so the muxing # loop below can consume. - first_keyframe = next(filter(is_keyframe, filter(is_video, container_packets))) + first_keyframe = next( + filter(lambda pkt: is_keyframe(pkt) and is_video(pkt), container_packets) + ) # Deal with problem #1 above (bad first packet pts/dts) by recalculating # using pts/dts from second packet. Use the peek iterator to advance # without consuming from container_packets. Skip over the first keyframe # then use the duration from the second video packet to adjust dts. next_video_packet = next(filter(is_video, container_packets.peek())) - start_dts = next_video_packet.dts - next_video_packet.duration + # Since the is_valid filter has already been applied before the following + # adjustment, it does not filter out the case where the duration below is + # 0 and both the first_keyframe and next_video_packet end up with the same + # dts. Use "or 1" to deal with this. + start_dts = next_video_packet.dts - (next_video_packet.duration or 1) first_keyframe.dts = first_keyframe.pts = start_dts except (av.AVError, StopIteration) as ex: _LOGGER.error("Error demuxing stream while finding first packet: %s", str(ex)) From a75c7d52c984edf84332212d7245a221feaca8b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jul 2021 02:00:52 -0700 Subject: [PATCH 761/818] Cost sensor handle consumption sensor in Wh (#53746) --- homeassistant/components/energy/sensor.py | 24 ++++++ tests/components/energy/test_sensor.py | 89 +++++++++++++++++++++-- 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1c42ea5a050..e974035cbd6 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from functools import partial +import logging from typing import Any, Final, Literal, TypeVar, cast from homeassistant.components.sensor import ( @@ -11,6 +12,11 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, +) from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -20,6 +26,8 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN from .data import EnergyManager, async_get_manager +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -188,6 +196,12 @@ class EnergyCostSensor(SensorEntity): energy_price = float(energy_price_state.state) except ValueError: return + + if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( + f"/{ENERGY_WATT_HOUR}" + ): + energy_price *= 1000.0 + else: energy_price_state = None energy_price = cast(float, self._flow["number_energy_price"]) @@ -197,6 +211,16 @@ class EnergyCostSensor(SensorEntity): self._reset(energy_state) return + energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if energy_unit == ENERGY_WATT_HOUR: + energy_price /= 1000 + elif energy_unit != ENERGY_KILO_WATT_HOUR: + _LOGGER.warning( + "Found unexpected unit %s for %s", energy_unit, energy_state.entity_id + ) + return + if ( energy_state.attributes[ATTR_LAST_RESET] != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index f3af93c06c1..978b21e1919 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -16,6 +16,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_MONETARY, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -133,7 +135,9 @@ async def test_cost_sensor_price_entity( # Optionally initialize dependent entities if initial_energy is not None: hass.states.async_set( - usage_sensor_entity_id, initial_energy, {"last_reset": last_reset} + usage_sensor_entity_id, + initial_energy, + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) hass.states.async_set("sensor.energy_price", "1") @@ -152,7 +156,12 @@ async def test_cost_sensor_price_entity( if initial_energy is None: with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( - usage_sensor_entity_id, "0", {"last_reset": last_reset} + usage_sensor_entity_id, + "0", + { + "last_reset": last_reset, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + }, ) await hass.async_block_till_done() @@ -169,7 +178,11 @@ async def test_cost_sensor_price_entity( # # assert entry.unique_id == "energy_energy_consumption cost" # Energy use bumped to 10 kWh - hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "10", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR @@ -189,7 +202,11 @@ async def test_cost_sensor_price_entity( assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR # Additional consumption is using the new price - hass.states.async_set(usage_sensor_entity_id, "14.5", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "14.5", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR @@ -202,13 +219,21 @@ async def test_cost_sensor_price_entity( # Energy sensor is reset, with start point at 4kWh last_reset = (now + timedelta(seconds=1)).isoformat() - hass.states.async_set(usage_sensor_entity_id, "4", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "4", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" # 0 EUR + (4-4) kWh * 2 EUR/kWh = 0 EUR # Energy use bumped to 10 kWh - hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "10", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "12.0" # 0 EUR + (10-4) kWh * 2 EUR/kWh = 12 EUR @@ -218,3 +243,55 @@ async def test_cost_sensor_price_entity( statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) assert cost_sensor_entity_id in statistics assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 31.0 + + +async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: + """Test energy cost price from sensor entity.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.energy_consumption", + "entity_energy_from": "sensor.energy_consumption", + "stat_cost": None, + "entity_energy_price": None, + "number_energy_price": 0.5, + } + ], + "flow_to": [], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + + now = dt_util.utcnow() + last_reset = dt_util.utc_from_timestamp(0).isoformat() + + hass.states.async_set( + "sensor.energy_consumption", + 10000, + {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + ) + + with patch("homeassistant.util.dt.utcnow", return_value=now): + await setup_integration(hass) + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "0.0" + + # Energy use bumped to 10 kWh + hass.states.async_set( + "sensor.energy_consumption", + 20000, + {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "5.0" From fc8af9af8ee38aa3948cd69ad10292da219e8ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 30 Jul 2021 21:44:10 +0200 Subject: [PATCH 762/818] Revert "Rename snapshot -> backup" (#53751) This reverts commit 9806bda272bd855f27a2ebdda97e1725474ad03e. --- homeassistant/components/hassio/__init__.py | 57 +++++------------- homeassistant/components/hassio/http.py | 9 +-- homeassistant/components/hassio/services.yaml | 58 ++----------------- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/addon.py | 14 ++--- tests/components/hassio/test_http.py | 20 +++---- tests/components/hassio/test_init.py | 43 ++++---------- tests/components/hassio/test_websocket_api.py | 4 +- tests/components/zwave_js/conftest.py | 12 ++-- tests/components/zwave_js/test_init.py | 44 +++++++------- 10 files changed, 82 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 4541685061a..6c71f2eb042 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -89,8 +89,6 @@ SERVICE_HOST_SHUTDOWN = "host_shutdown" SERVICE_HOST_REBOOT = "host_reboot" SERVICE_SNAPSHOT_FULL = "snapshot_full" SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" -SERVICE_BACKUP_FULL = "backup_full" -SERVICE_BACKUP_PARTIAL = "backup_partial" SERVICE_RESTORE_FULL = "restore_full" SERVICE_RESTORE_PARTIAL = "restore_partial" @@ -103,11 +101,11 @@ SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend( {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)} ) -SCHEMA_BACKUP_FULL = vol.Schema( +SCHEMA_SNAPSHOT_FULL = vol.Schema( {vol.Optional(ATTR_NAME): cv.string, vol.Optional(ATTR_PASSWORD): cv.string} ) -SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( +SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend( { vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), @@ -115,12 +113,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( ) SCHEMA_RESTORE_FULL = vol.Schema( - { - vol.Exclusive(ATTR_SLUG, ATTR_SLUG): cv.slug, - vol.Exclusive(ATTR_SNAPSHOT, ATTR_SLUG): cv.slug, - vol.Optional(ATTR_PASSWORD): cv.string, - }, - cv.has_at_least_one_key(ATTR_SLUG, ATTR_SNAPSHOT), + {vol.Required(ATTR_SNAPSHOT): cv.slug, vol.Optional(ATTR_PASSWORD): cv.string} ) SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( @@ -140,32 +133,25 @@ MAP_SERVICE_API = { SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False), SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False), SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False), - SERVICE_BACKUP_FULL: ("/backups/new/full", SCHEMA_BACKUP_FULL, 300, True), - SERVICE_BACKUP_PARTIAL: ( - "/backups/new/partial", - SCHEMA_BACKUP_PARTIAL, + SERVICE_SNAPSHOT_FULL: ("/snapshots/new/full", SCHEMA_SNAPSHOT_FULL, 300, True), + SERVICE_SNAPSHOT_PARTIAL: ( + "/snapshots/new/partial", + SCHEMA_SNAPSHOT_PARTIAL, 300, True, ), SERVICE_RESTORE_FULL: ( - "/backups/{slug}/restore/full", + "/snapshots/{snapshot}/restore/full", SCHEMA_RESTORE_FULL, 300, True, ), SERVICE_RESTORE_PARTIAL: ( - "/backups/{slug}/restore/partial", + "/snapshots/{snapshot}/restore/partial", SCHEMA_RESTORE_PARTIAL, 300, True, ), - SERVICE_SNAPSHOT_FULL: ("/backups/new/full", SCHEMA_BACKUP_FULL, 300, True), - SERVICE_SNAPSHOT_PARTIAL: ( - "/backups/new/partial", - SCHEMA_BACKUP_PARTIAL, - 300, - True, - ), } @@ -286,16 +272,16 @@ async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict @bind_hass @api_data -async def async_create_backup( +async def async_create_snapshot( hass: HomeAssistant, payload: dict, partial: bool = False ) -> dict: - """Create a full or partial backup. + """Create a full or partial snapshot. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] - backup_type = "partial" if partial else "full" - command = f"/backups/new/{backup_type}" + snapshot_type = "partial" if partial else "full" + command = f"/snapshots/new/{snapshot_type}" return await hassio.send_command(command, payload=payload, timeout=None) @@ -467,22 +453,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_service_handler(service): """Handle service calls for Hass.io.""" api_command = MAP_SERVICE_API[service.service][0] - if "snapshot" in service.service: - _LOGGER.warning( - "The service '%s' is deprecated and will be removed in Home Assistant 2021.10, use '%s' instead", - service.service, - service.service.replace("snapshot", "backup"), - ) data = service.data.copy() addon = data.pop(ATTR_ADDON, None) - slug = data.pop(ATTR_SLUG, None) snapshot = data.pop(ATTR_SNAPSHOT, None) - if snapshot is not None: - _LOGGER.warning( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.10, use 'slug' instead" - ) - slug = snapshot - payload = None # Pass data to Hass.io API @@ -494,12 +467,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Call API try: await hassio.send_command( - api_command.format(addon=addon, slug=slug), + api_command.format(addon=addon, snapshot=snapshot), payload=payload, timeout=MAP_SERVICE_API[service.service][2], ) except HassioAPIError as err: - _LOGGER.error("Error on Supervisor API: %s", err) + _LOGGER.error("Error on Hass.io API: %s", err) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 302cc00bb9f..47131b80de3 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -29,9 +29,6 @@ NO_TIMEOUT = re.compile( r"|hassos/update/cli" r"|supervisor/update" r"|addons/[^/]+/(?:update|install|rebuild)" - r"|backups/.+/full" - r"|backups/.+/partial" - r"|backups/[^/]+/(?:upload|download)" r"|snapshots/.+/full" r"|snapshots/.+/partial" r"|snapshots/[^/]+/(?:upload|download)" @@ -39,7 +36,7 @@ NO_TIMEOUT = re.compile( ) NO_AUTH_ONBOARDING = re.compile( - r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r"|snapshots/[^/]+/.+" r")$" + r"^(?:" r"|supervisor/logs" r"|snapshots/[^/]+/.+" r")$" ) NO_AUTH = re.compile( @@ -84,13 +81,13 @@ class HassIOView(HomeAssistantView): client_timeout = 10 data = None headers = _init_header(request) - if path in ("snapshots/new/upload", "backups/new/upload"): + if path == "snapshots/new/upload": # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access - # Backups are big, so we need to adjust the allowed size + # Snapshots are big, so we need to adjust the allowed size request._client_max_size = ( # pylint: disable=protected-access MAX_UPLOAD_SIZE ) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 38d78984ddc..0652b65d6e2 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -67,13 +67,13 @@ host_shutdown: description: Poweroff the host system. snapshot_full: - name: Create a full backup. - description: Create a full backup (deprecated, use backup_full instead). + name: Create a full snapshot. + description: Create a full snapshot. fields: name: name: Name description: Optional or it will be the current date and time. - example: "backup 1" + example: "Snapshot 1" selector: text: password: @@ -84,8 +84,8 @@ snapshot_full: text: snapshot_partial: - name: Create a partial backup. - description: Create a partial backup (deprecated, use backup_partial instead). + name: Create a partial snapshot. + description: Create a partial snapshot. fields: addons: name: Add-ons @@ -102,53 +102,7 @@ snapshot_partial: name: name: Name description: Optional or it will be the current date and time. - example: "Partial backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - -backup_full: - name: Create a full backup. - description: Create a full backup. - fields: - name: - name: Name - description: Optional or it will be the current date and time. - example: "backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - -backup_partial: - name: Create a partial backup. - description: Create a partial backup. - fields: - addons: - name: Add-ons - description: Optional list of addon slugs. - example: ["core_ssh", "core_samba", "core_mosquitto"] - selector: - object: - folders: - name: Folders - description: Optional list of directories. - example: ["homeassistant", "share"] - selector: - object: - name: - name: Name - description: Optional or it will be the current date and time. - example: "Partial backup 1" + example: "Partial Snapshot 1" selector: text: password: diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6320efddb60..6cd0104e298 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -547,7 +547,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: LOGGER.error(err) return try: - await addon_manager.async_create_backup() + await addon_manager.async_create_snapshot() except AddonError as err: LOGGER.error(err) return diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 29ae887b4bc..a0caaa15488 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -8,7 +8,7 @@ from functools import partial from typing import Any, Callable, TypeVar, cast from homeassistant.components.hassio import ( - async_create_backup, + async_create_snapshot, async_get_addon_discovery_info, async_get_addon_info, async_install_addon, @@ -202,7 +202,7 @@ class AddonManager: if not addon_info.update_available: return - await self.async_create_backup() + await self.async_create_snapshot() await async_update_addon(self._hass, ADDON_SLUG) @callback @@ -289,14 +289,14 @@ class AddonManager: ) return self._start_task - @api_error("Failed to create a backup of the Z-Wave JS add-on.") - async def async_create_backup(self) -> None: - """Create a partial backup of the Z-Wave JS add-on.""" + @api_error("Failed to create a snapshot of the Z-Wave JS add-on.") + async def async_create_snapshot(self) -> None: + """Create a partial snapshot of the Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() name = f"addon_{ADDON_SLUG}_{addon_info.version}" - LOGGER.debug("Creating backup: %s", name) - await async_create_backup( + LOGGER.debug("Creating snapshot: %s", name) + await async_create_snapshot( self._hass, {"name": name, "addons": [ADDON_SLUG]}, partial=True, diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index fc4bb3e6a0d..ff1c348a37b 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -132,13 +132,13 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo assert req_headers["X-Hass-Is-Admin"] == "1" -async def test_backup_upload_headers(hassio_client, aioclient_mock, caplog): - """Test that we forward the full header for backup upload.""" +async def test_snapshot_upload_headers(hassio_client, aioclient_mock): + """Test that we forward the full header for snapshot upload.""" content_type = "multipart/form-data; boundary='--webkit'" - aioclient_mock.get("http://127.0.0.1/backups/new/upload") + aioclient_mock.get("http://127.0.0.1/snapshots/new/upload") resp = await hassio_client.get( - "/api/hassio/backups/new/upload", headers={"Content-Type": content_type} + "/api/hassio/snapshots/new/upload", headers={"Content-Type": content_type} ) # Check we got right response @@ -150,18 +150,18 @@ async def test_backup_upload_headers(hassio_client, aioclient_mock, caplog): assert req_headers["Content-Type"] == content_type -async def test_backup_download_headers(hassio_client, aioclient_mock): - """Test that we forward the full header for backup download.""" +async def test_snapshot_download_headers(hassio_client, aioclient_mock): + """Test that we forward the full header for snapshot download.""" content_disposition = "attachment; filename=test.tar" aioclient_mock.get( - "http://127.0.0.1/backups/slug/download", + "http://127.0.0.1/snapshots/slug/download", headers={ "Content-Length": "50000000", "Content-Disposition": content_disposition, }, ) - resp = await hassio_client.get("/api/hassio/backups/slug/download") + resp = await hassio_client.get("/api/hassio/snapshots/slug/download") # Check we got right response assert resp.status == 200 @@ -174,9 +174,9 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") - assert _need_auth(hass, "backups/new/upload") + assert _need_auth(hass, "snapshots/new/upload") assert _need_auth(hass, "supervisor/logs") hass.data["onboarding"] = False - assert not _need_auth(hass, "backups/new/upload") + assert not _need_auth(hass, "snapshots/new/upload") assert not _need_auth(hass, "supervisor/logs") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 910ed12cb52..8377e5287d0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -303,13 +303,11 @@ async def test_service_register(hassio_env, hass): assert hass.services.has_service("hassio", "host_reboot") assert hass.services.has_service("hassio", "snapshot_full") assert hass.services.has_service("hassio", "snapshot_partial") - assert hass.services.has_service("hassio", "backup_full") - assert hass.services.has_service("hassio", "backup_partial") assert hass.services.has_service("hassio", "restore_full") assert hass.services.has_service("hassio", "restore_partial") -async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): +async def test_service_calls(hassio_env, hass, aioclient_mock): """Call service and check the API calls behind that.""" assert await async_setup_component(hass, "hassio", {}) @@ -320,13 +318,13 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): aioclient_mock.post("http://127.0.0.1/addons/test/stdin", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/host/shutdown", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/host/reboot", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/backups/new/full", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/backups/new/partial", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/snapshots/new/full", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/snapshots/new/partial", json={"result": "ok"}) aioclient_mock.post( - "http://127.0.0.1/backups/test/restore/full", json={"result": "ok"} + "http://127.0.0.1/snapshots/test/restore/full", json={"result": "ok"} ) aioclient_mock.post( - "http://127.0.0.1/backups/test/restore/partial", json={"result": "ok"} + "http://127.0.0.1/snapshots/test/restore/partial", json={"result": "ok"} ) await hass.services.async_call("hassio", "addon_start", {"addon": "test"}) @@ -347,48 +345,27 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): assert aioclient_mock.call_count == 10 - await hass.services.async_call("hassio", "backup_full", {}) - await hass.services.async_call( - "hassio", - "backup_partial", - {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, - ) await hass.services.async_call("hassio", "snapshot_full", {}) await hass.services.async_call( "hassio", "snapshot_partial", - {"addons": ["test"], "folders": ["ssl"]}, + {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, ) await hass.async_block_till_done() - assert ( - "The service 'snapshot_full' is deprecated and will be removed in Home Assistant 2021.10, use 'backup_full' instead" - in caplog.text - ) - assert ( - "The service 'snapshot_partial' is deprecated and will be removed in Home Assistant 2021.10, use 'backup_partial' instead" - in caplog.text - ) - assert aioclient_mock.call_count == 14 - assert aioclient_mock.mock_calls[-3][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], "password": "123456", } - await hass.services.async_call("hassio", "restore_full", {"slug": "test"}) await hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) - await hass.async_block_till_done() - assert ( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.10, use 'slug' instead" - in caplog.text - ) - await hass.services.async_call( "hassio", "restore_partial", { - "slug": "test", + "snapshot": "test", "homeassistant": False, "addons": ["test"], "folders": ["ssl"], @@ -397,7 +374,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 17 + assert aioclient_mock.call_count == 14 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5578194b87c..5278d2cbb91 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -61,7 +61,7 @@ async def test_websocket_supervisor_api( assert await async_setup_component(hass, "hassio", {}) websocket_client = await hass_ws_client(hass) aioclient_mock.post( - "http://127.0.0.1/backups/new/partial", + "http://127.0.0.1/snapshots/new/partial", json={"result": "ok", "data": {"slug": "sn_slug"}}, ) @@ -69,7 +69,7 @@ async def test_websocket_supervisor_api( { WS_ID: 1, WS_TYPE: WS_TYPE_API, - ATTR_ENDPOINT: "/backups/new/partial", + ATTR_ENDPOINT: "/snapshots/new/partial", ATTR_METHOD: "post", } ) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 75b5ab65d38..0f336e396fe 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -171,13 +171,13 @@ def uninstall_addon_fixture(): yield uninstall_addon -@pytest.fixture(name="create_backup") -def create_backup_fixture(): - """Mock create backup.""" +@pytest.fixture(name="create_shapshot") +def create_snapshot_fixture(): + """Mock create snapshot.""" with patch( - "homeassistant.components.zwave_js.addon.async_create_backup" - ) as create_backup: - yield create_backup + "homeassistant.components.zwave_js.addon.async_create_snapshot" + ) as create_shapshot: + yield create_shapshot @pytest.fixture(name="controller_state", scope="session") diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 447b052b8c0..0b9009cd1d7 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -365,8 +365,8 @@ async def test_addon_options_changed( @pytest.mark.parametrize( - "addon_version, update_available, update_calls, backup_calls, " - "update_addon_side_effect, create_backup_side_effect", + "addon_version, update_available, update_calls, snapshot_calls, " + "update_addon_side_effect, create_shapshot_side_effect", [ ("1.0", True, 1, 1, None, None), ("1.0", False, 0, 0, None, None), @@ -380,15 +380,15 @@ async def test_update_addon( addon_info, addon_installed, addon_running, - create_backup, + create_shapshot, update_addon, addon_options, addon_version, update_available, update_calls, - backup_calls, + snapshot_calls, update_addon_side_effect, - create_backup_side_effect, + create_shapshot_side_effect, ): """Test update the Z-Wave JS add-on during entry setup.""" device = "/test" @@ -397,7 +397,7 @@ async def test_update_addon( addon_options["network_key"] = network_key addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available - create_backup.side_effect = create_backup_side_effect + create_shapshot.side_effect = create_shapshot_side_effect update_addon.side_effect = update_addon_side_effect client.connect.side_effect = InvalidServerVersion("Invalid version") entry = MockConfigEntry( @@ -416,7 +416,7 @@ async def test_update_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert create_backup.call_count == backup_calls + assert create_shapshot.call_count == snapshot_calls assert update_addon.call_count == update_calls @@ -469,7 +469,7 @@ async def test_stop_addon( async def test_remove_entry( - hass, addon_installed, stop_addon, create_backup, uninstall_addon, caplog + hass, addon_installed, stop_addon, create_shapshot, uninstall_addon, caplog ): """Test remove the config entry.""" # test successful remove without created add-on @@ -500,8 +500,8 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 1 - assert create_backup.call_args == call( + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, @@ -511,7 +511,7 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() - create_backup.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() # test add-on stop failure @@ -523,27 +523,27 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 0 + assert create_shapshot.call_count == 0 assert uninstall_addon.call_count == 0 assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the Z-Wave JS add-on" in caplog.text stop_addon.side_effect = None stop_addon.reset_mock() - create_backup.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() - # test create backup failure + # test create snapshot failure entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - create_backup.side_effect = HassioAPIError() + create_shapshot.side_effect = HassioAPIError() await hass.config_entries.async_remove(entry.entry_id) assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 1 - assert create_backup.call_args == call( + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, @@ -551,10 +551,10 @@ async def test_remove_entry( assert uninstall_addon.call_count == 0 assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to create a backup of the Z-Wave JS add-on" in caplog.text - create_backup.side_effect = None + assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text + create_shapshot.side_effect = None stop_addon.reset_mock() - create_backup.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() # test add-on uninstall failure @@ -566,8 +566,8 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 1 - assert create_backup.call_args == call( + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, From 423fb910b588239a86ad2d4d0c6a0dd9e40ebaf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jul 2021 14:44:28 -0500 Subject: [PATCH 763/818] Bump HAP-python to 3.6.0 (#53754) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_cameras.py | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 5ec935611f7..887dfc3ee37 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.5.2", + "HAP-python==3.6.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 75313a53947..1e469e39839 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.5.2 +HAP-python==3.6.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a49e3faae2..b0c2223e7d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.5.2 +HAP-python==3.6.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 354db900470..b9df572a699 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -696,21 +696,21 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): char = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) assert char - assert char.value == 0 + assert char.value is None service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH) assert service2 char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) assert char2 - assert char2.value == 0 + assert char2.value is None hass.states.async_set( doorbell_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} ) await hass.async_block_till_done() - assert char.value == 0 - assert char2.value == 0 + assert char.value is None + assert char2.value is None char.set_value(True) char2.set_value(True) @@ -718,8 +718,8 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} ) await hass.async_block_till_done() - assert char.value == 0 - assert char2.value == 0 + assert char.value is None + assert char2.value is None # Ensure we do not throw when the linked # doorbell sensor is removed @@ -727,8 +727,8 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await hass.async_block_till_done() await acc.run() await hass.async_block_till_done() - assert char.value == 0 - assert char2.value == 0 + assert char.value is None + assert char2.value is None async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, events): From 0aee659ee94990b11d4df62c352c86663abc91bb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 30 Jul 2021 21:44:52 +0200 Subject: [PATCH 764/818] Fix Xiaomi Miio humidifier mode change (#53757) --- homeassistant/components/xiaomi_miio/humidifier.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index eb45a716254..64248f900e0 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -232,7 +232,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): """Return the target humidity.""" return ( self._target_humidity - if self._mode == AirhumidifierOperationMode.Auto.name + if self._mode == AirhumidifierOperationMode.Auto.value or AirhumidifierOperationMode.Auto.name not in self.available_modes else None ) @@ -264,7 +264,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): self._device.set_mode, AirhumidifierOperationMode.Auto, ): - self._mode = AirhumidifierOperationMode.Auto.name + self._mode = AirhumidifierOperationMode.Auto.value self.async_write_ha_state() async def async_set_mode(self, mode: str) -> None: @@ -280,9 +280,9 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - AirhumidifierOperationMode[mode.title()], + AirhumidifierOperationMode[mode], ): - self._mode = mode.title() + self._mode = mode.lower() self.async_write_ha_state() From 958df580a9b73de90c791443aa2745c2565a6edf Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Jul 2021 21:47:55 +0200 Subject: [PATCH 765/818] Fix Xiaomi-miio humidifier write the state back when turning on or off (#53771) --- homeassistant/components/xiaomi_miio/humidifier.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 64248f900e0..aee2c237066 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -139,6 +139,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): ) if result: self._state = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" @@ -148,6 +149,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): if result: self._state = False + self.async_write_ha_state() def translate_humidity(self, humidity): """Translate the target humidity to the first valid step.""" From 9f0f40dac6036a6ff0661dd91494f79807a83bd9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 30 Jul 2021 15:13:53 -0600 Subject: [PATCH 766/818] Fix parsing of non-string values in Slack data (#53775) --- homeassistant/components/slack/notify.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 0eadc26075e..4dfacda266c 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -146,20 +146,6 @@ def _async_sanitize_channel_names(channel_list: list[str]) -> list[str]: return [channel.lstrip("#") for channel in channel_list] -@callback -def _async_templatize_blocks(hass: HomeAssistant, value: Any) -> Any: - """Recursive template creator helper function.""" - if isinstance(value, list): - return [_async_templatize_blocks(hass, item) for item in value] - if isinstance(value, dict): - return { - key: _async_templatize_blocks(hass, item) for key, item in value.items() - } - - tmpl = template.Template(value, hass=hass) # type: ignore # no-untyped-call - return tmpl.async_render(parse_result=False) - - class SlackNotificationService(BaseNotificationService): """Define the Slack notification logic.""" @@ -314,9 +300,9 @@ class SlackNotificationService(BaseNotificationService): # Message Type 1: A text-only message if ATTR_FILE not in data: if ATTR_BLOCKS_TEMPLATE in data: - blocks = _async_templatize_blocks( - self._hass, data[ATTR_BLOCKS_TEMPLATE] - ) + value = cv.template_complex(data[ATTR_BLOCKS_TEMPLATE]) + template.attach(self._hass, value) + blocks = template.render_complex(value) elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: From af96c5d60c0bbfb0a90d69908c205b6b1cbe808d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Jul 2021 23:11:47 +0200 Subject: [PATCH 767/818] Update frontend to 20210730.0 (#53778) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ac791977038..02ee134523e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210729.0" + "home-assistant-frontend==20210730.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e1acf1dba15..1418f59fedf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210729.0 +home-assistant-frontend==20210730.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1e469e39839..517e71c9b73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210729.0 +home-assistant-frontend==20210730.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0c2223e7d2..03412d8e729 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210729.0 +home-assistant-frontend==20210730.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From cd3390e01273f8692997d74bf00ddccf40a733fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jul 2021 14:14:58 -0700 Subject: [PATCH 768/818] Bump Hue and only fire events for button presses (#53781) * Bump Hue and only fire events for button presses * Fix tests --- homeassistant/components/hue/hue_event.py | 12 +++++++++++- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_sensor_base.py | 3 +++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index 7c0163f8a16..6bd68b106bb 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -1,7 +1,12 @@ """Representation of a Hue remote firing events for button presses.""" import logging -from aiohue.sensors import TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, TYPE_ZLL_SWITCH +from aiohue.sensors import ( + EVENT_BUTTON, + TYPE_ZGP_SWITCH, + TYPE_ZLL_ROTARY, + TYPE_ZLL_SWITCH, +) from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback @@ -50,6 +55,11 @@ class HueEvent(GenericHueDevice): """Fire the event if reason is that state is updated.""" if ( self.sensor.state == self._last_state + # Filter out non-button events if last event type is available + or ( + self.sensor.last_event is not None + and self.sensor.last_event["type"] != EVENT_BUTTON + ) or # Filter out old states. Can happen when events fire while refreshing dt_util.parse_datetime(self.sensor.state["lastupdated"]) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 3c8078364ab..05e69948218 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.5.1"], + "requirements": ["aiohue==2.6.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 517e71c9b73..a2a24953b71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.5.1 +aiohue==2.6.0 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03412d8e729..4c934e07675 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -122,7 +122,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.5.1 +aiohue==2.6.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index bc11c013555..b8e9c83e47d 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -446,6 +446,9 @@ async def test_hue_events(hass, mock_bridge): assert len(hass.states.async_all()) == 7 assert len(events) == 0 + mock_bridge.api.sensors["7"].last_event = {"type": "button"} + mock_bridge.api.sensors["8"].last_event = {"type": "button"} + new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response["7"]["state"] = { "buttonevent": 18, From e0fc14f82c8f121fb11f494b98e4b72e3beb2ea0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jul 2021 14:15:42 -0700 Subject: [PATCH 769/818] Bumped version to 2021.8.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bc79728c307..9a3f47848bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2297c0b58b4e141be28aaebac77571fd66be4d32 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 1 Aug 2021 23:58:55 +0200 Subject: [PATCH 770/818] Do not block setup of TP-Link when device unreachable (#53770) --- homeassistant/components/tplink/__init__.py | 70 ++++++++++++++++---- homeassistant/components/tplink/const.py | 2 + tests/components/tplink/consts.py | 29 +++++++- tests/components/tplink/test_init.py | 73 ++++++++++++++------- 4 files changed, 137 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index e309d2c5082..88160722669 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta import logging import time +from typing import Any from pyHS100.smartdevice import SmartDevice, SmartDeviceException from pyHS100.smartplug import SmartPlug @@ -22,9 +23,9 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.dt import utc_from_timestamp @@ -44,6 +45,8 @@ from .const import ( CONF_SWITCH, COORDINATORS, PLATFORMS, + UNAVAILABLE_DEVICES, + UNAVAILABLE_RETRY_DELAY, ) _LOGGER = logging.getLogger(__name__) @@ -96,16 +99,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" config_data = hass.data[DOMAIN].get(ATTR_CONFIG) + if config_data is None and entry.data: + config_data = entry.data + elif config_data is not None: + hass.config_entries.async_update_entry(entry, data=config_data) device_registry = dr.async_get(hass) tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) device_count = len(tplink_devices) + hass_data: dict[str, Any] = hass.data[DOMAIN] # These will contain the initialized devices - hass.data[DOMAIN][CONF_LIGHT] = [] - hass.data[DOMAIN][CONF_SWITCH] = [] - lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT] - switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH] + hass_data[CONF_LIGHT] = [] + hass_data[CONF_SWITCH] = [] + hass_data[UNAVAILABLE_DEVICES] = [] + lights: list[SmartDevice] = hass_data[CONF_LIGHT] + switches: list[SmartPlug] = hass_data[CONF_SWITCH] + unavailable_devices: list[SmartDevice] = hass_data[UNAVAILABLE_DEVICES] # Add static devices static_devices = SmartDevices() @@ -136,22 +146,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ", ".join(d.host for d in switches), ) + async def async_retry_devices(self) -> None: + """Retry unavailable devices.""" + unavailable_devices: list[SmartDevice] = hass_data[UNAVAILABLE_DEVICES] + _LOGGER.debug( + "retry during setup unavailable devices: %s", + [d.host for d in unavailable_devices], + ) + + for device in unavailable_devices: + try: + device.get_sysinfo() + except SmartDeviceException: + continue + _LOGGER.debug( + "at least one device is available again, so reload integration" + ) + await hass.config_entries.async_reload(entry.entry_id) + break + # prepare DataUpdateCoordinators - hass.data[DOMAIN][COORDINATORS] = {} + hass_data[COORDINATORS] = {} for switch in switches: try: await hass.async_add_executor_job(switch.get_sysinfo) - except SmartDeviceException as ex: - _LOGGER.debug(ex) - raise ConfigEntryNotReady from ex + except SmartDeviceException: + _LOGGER.warning( + "Device at '%s' not reachable during setup, will retry later", + switch.host, + ) + unavailable_devices.append(switch) + continue - hass.data[DOMAIN][COORDINATORS][ + hass_data[COORDINATORS][ switch.mac ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) await coordinator.async_config_entry_first_refresh() + if unavailable_devices: + entry.async_on_unload( + async_track_time_interval( + hass, async_retry_devices, UNAVAILABLE_RETRY_DELAY + ) + ) + unavailable_devices_hosts = [d.host for d in unavailable_devices] + hass_data[CONF_SWITCH] = [ + s for s in switches if s.host not in unavailable_devices_hosts + ] + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -159,10 +203,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - platforms = [platform for platform in PLATFORMS if hass.data[DOMAIN].get(platform)] - unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + hass_data: dict[str, Any] = hass.data[DOMAIN] if unload_ok: - hass.data[DOMAIN].clear() + hass_data.clear() return unload_ok diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 888d671096d..60e06fd1ffe 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -5,6 +5,8 @@ import datetime DOMAIN = "tplink" COORDINATORS = "coordinators" +UNAVAILABLE_DEVICES = "unavailable_devices" +UNAVAILABLE_RETRY_DELAY = datetime.timedelta(seconds=300) MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) MAX_DISCOVERY_RETRIES = 4 diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py index de134ddbe07..95177a12a9c 100644 --- a/tests/components/tplink/consts.py +++ b/tests/components/tplink/consts.py @@ -1,6 +1,6 @@ """Constants for the TP-Link component tests.""" -SMARTPLUGSWITCH_DATA = { +SMARTPLUG_HS110_DATA = { "sysinfo": { "sw_ver": "1.0.4 Build 191111 Rel.143500", "hw_ver": "4.0", @@ -34,6 +34,33 @@ SMARTPLUGSWITCH_DATA = { "err_code": 0, }, } +SMARTPLUG_HS100_DATA = { + "sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS100(EU)", + "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", + "oemId": "40F54B43071E9436B6395611E9D91CEA", + "hwId": "A6C77E4FDD238B53D824AC8DA361F043", + "rssi": -24, + "longitude_i": 130793, + "latitude_i": 480582, + "alias": "SmartPlug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM:", + "mac": "A9:F4:3D:A4:E3:47", + "updating": 0, + "led_off": 0, + "relay_state": 0, + "on_time": 0, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug", + "next_action": {"type": -1}, + "err_code": 0, + } +} SMARTSTRIPWITCH_DATA = { "sysinfo": { "sw_ver": "1.0.4 Build 191111 Rel.143500", diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index fb3f44709fc..a201788f35b 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -11,6 +11,8 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.common import SmartDevices from homeassistant.components.tplink.const import ( CONF_DIMMER, @@ -19,16 +21,21 @@ from homeassistant.components.tplink.const import ( CONF_SW_VERSION, CONF_SWITCH, COORDINATORS, + UNAVAILABLE_RETRY_DELAY, ) from homeassistant.components.tplink.sensor import ENERGY_SENSORS from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from homeassistant.util import slugify +from homeassistant.util import dt, slugify -from tests.common import MockConfigEntry, mock_coro -from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA +from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro +from tests.components.tplink.consts import ( + SMARTPLUG_HS100_DATA, + SMARTPLUG_HS110_DATA, + SMARTSTRIPWITCH_DATA, +) async def test_creating_entry_tries_discover(hass): @@ -220,9 +227,9 @@ async def test_platforms_are_initialized(hass: HomeAssistant): light = SmartBulb("123.123.123.123") switch = SmartPlug("321.321.321.321") - switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS110_DATA["sysinfo"]) switch.get_emeter_realtime = MagicMock( - return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"]) ) switch.get_emeter_daily = MagicMock( return_value={int(time.strftime("%e")): 1.123} @@ -270,25 +277,22 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ), patch( - "homeassistant.components.tplink.switch.async_setup_entry", - return_value=mock_coro(True), ), patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): switch = SmartPlug("321.321.321.321") - switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS100_DATA["sysinfo"]) get_static_devices.return_value = SmartDevices([], [switch]) await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - for description in ENERGY_SENSORS: - state = hass.states.get( - f"sensor.{switch.alias}_{slugify(description.name)}" - ) - assert state is None + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 1 + + entities = hass.states.async_entity_ids(SENSOR_DOMAIN) + assert len(entities) == 0 async def test_smartstrip_device(hass: HomeAssistant): @@ -346,8 +350,8 @@ async def test_no_config_creates_no_entry(hass): assert mock_setup.call_count == 0 -async def test_not_ready(hass: HomeAssistant): - """Test for not ready when configured devices are not available.""" +async def test_not_available_at_startup(hass: HomeAssistant): + """Test when configured devices are not available.""" config = { tplink.DOMAIN: { CONF_DISCOVERY: False, @@ -362,9 +366,6 @@ async def test_not_ready(hass: HomeAssistant): ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ), patch( - "homeassistant.components.tplink.switch.async_setup_entry", - return_value=mock_coro(True), ), patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): @@ -373,13 +374,39 @@ async def test_not_ready(hass: HomeAssistant): switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) get_static_devices.return_value = SmartDevices([], [switch]) + # run setup while device unreachable await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() entries = hass.config_entries.async_entries(tplink.DOMAIN) - assert len(entries) == 1 - assert entries[0].state is config_entries.ConfigEntryState.SETUP_RETRY + assert entries[0].state is config_entries.ConfigEntryState.LOADED + + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 0 + + # retrying with still unreachable device + async_fire_time_changed(hass, dt.utcnow() + UNAVAILABLE_RETRY_DELAY) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(tplink.DOMAIN) + assert len(entries) == 1 + assert entries[0].state is config_entries.ConfigEntryState.LOADED + + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 0 + + # retrying with now reachable device + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS100_DATA["sysinfo"]) + async_fire_time_changed(hass, dt.utcnow() + UNAVAILABLE_RETRY_DELAY) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(tplink.DOMAIN) + assert len(entries) == 1 + assert entries[0].state is config_entries.ConfigEntryState.LOADED + + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 1 @pytest.mark.parametrize("platform", ["switch", "light"]) @@ -406,9 +433,9 @@ async def test_unload(hass, platform): light = SmartBulb("123.123.123.123") switch = SmartPlug("321.321.321.321") - switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS110_DATA["sysinfo"]) switch.get_emeter_realtime = MagicMock( - return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"]) ) if platform == "light": get_static_devices.return_value = SmartDevices([light], []) From 31869cbb12ae36772c1df43d39e1088bf4c047a9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 31 Jul 2021 12:32:16 +0200 Subject: [PATCH 771/818] Fix name migration of the Xiaomi Miio humidifier (#53790) --- homeassistant/components/xiaomi_miio/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e858c7ee797..36ee89ba7a0 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -99,7 +99,6 @@ async def async_create_miio_device_and_coordinator( token = entry.data[CONF_TOKEN] name = entry.title device = None - migrate_entity_name = None if model not in MODELS_HUMIDIFIER: return @@ -116,8 +115,8 @@ async def async_create_miio_device_and_coordinator( entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future - migrate_entity_name = entity_registry.async_get(entity_id).name - hass.config_entries.async_update_entry(entry, title=migrate_entity_name) + if migrate_entity_name := entity_registry.async_get(entity_id).name: + hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) async def async_update_data(): From 0948eafb93c480beb820596bdf86cae3c63077e1 Mon Sep 17 00:00:00 2001 From: Andreas Brett Date: Sat, 31 Jul 2021 14:47:51 +0200 Subject: [PATCH 772/818] Fix onkyo UnboundLocalError (#53793) audio_information_raw and video_information_raw were in some cases used before being assigned error: UnboundLocalError: local variable 'video_information_raw' referenced before assignment --- homeassistant/components/onkyo/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 2e4b6eff6da..ef20c1054f3 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -319,8 +319,10 @@ class OnkyoDevice(MediaPlayerEntity): preset_raw = self.command("preset query") if self._audio_info_supported: audio_information_raw = self.command("audio-information query") + self._parse_audio_information(audio_information_raw) if self._video_info_supported: video_information_raw = self.command("video-information query") + self._parse_video_information(video_information_raw) if not (volume_raw and mute_raw and current_source_raw): return @@ -343,9 +345,6 @@ class OnkyoDevice(MediaPlayerEntity): self._receiver_max_volume * self._max_volume / 100 ) - self._parse_audio_information(audio_information_raw) - self._parse_video_information(video_information_raw) - if not hdmi_out_raw: return self._attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) From fc5c30775dd2779c1051db3e6d1388cce1e463da Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 1 Aug 2021 23:58:31 +0200 Subject: [PATCH 773/818] Remove `led` from Xiaomi Miio humidifier features (#53796) --- homeassistant/components/xiaomi_miio/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 0d8d5bc0014..d250d8d4d2b 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -269,7 +269,6 @@ FEATURE_FLAGS_AIRPURIFIER_V3 = ( FEATURE_FLAGS_AIRHUMIDIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_TARGET_HUMIDITY ) From fd0ae7ab360eb7e7182ace3389ee812a3b6f7198 Mon Sep 17 00:00:00 2001 From: B-Hartley Date: Sun, 1 Aug 2021 23:01:34 +0100 Subject: [PATCH 774/818] ForecastSolar - power production now w not k w (#53797) --- homeassistant/components/forecast_solar/const.py | 11 ++++------- tests/components/forecast_solar/test_sensor.py | 8 ++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 7f426d2847c..7ae6fe01d42 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -53,7 +53,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_now", name="Estimated Power Production - Now", device_class=DEVICE_CLASS_POWER, - state=lambda estimate: estimate.power_production_now / 1000, + state=lambda estimate: estimate.power_production_now, state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), @@ -61,8 +61,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_next_hour", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) - ) - / 1000, + ), name="Estimated Power Production - Next Hour", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -72,8 +71,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_next_12hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) - ) - / 1000, + ), name="Estimated Power Production - Next 12 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -83,8 +81,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_next_24hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) - ) - / 1000, + ), name="Estimated Power Production - Next 24 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 8b8c1cc933e..a2b105ccbd1 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -96,7 +96,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_production_now" - assert state.state == "300.0" + assert state.state == "300000" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" ) @@ -175,17 +175,17 @@ async def test_disabled_by_default( ( "power_production_next_12hours", "Estimated Power Production - Next 12 Hours", - "600.0", + "600000", ), ( "power_production_next_24hours", "Estimated Power Production - Next 24 Hours", - "700.0", + "700000", ), ( "power_production_next_hour", "Estimated Power Production - Next Hour", - "400.0", + "400000", ), ], ) From 3f7ddb4706c1ffaa482e06f5f2f37a223b16d480 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 31 Jul 2021 21:19:00 +0200 Subject: [PATCH 775/818] Clean Xiaomi Miio humidifier services (#53806) --- homeassistant/components/xiaomi_miio/const.py | 5 --- .../components/xiaomi_miio/services.yaml | 43 ------------------- 2 files changed, 48 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index d250d8d4d2b..a2f7679bf1b 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -147,8 +147,6 @@ MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" SERVICE_SET_BUZZER = "set_buzzer" -SERVICE_SET_CLEAN_ON = "set_clean_on" -SERVICE_SET_CLEAN_OFF = "set_clean_off" SERVICE_SET_CLEAN = "set_clean" SERVICE_SET_FAN_LED_ON = "fan_set_led_on" SERVICE_SET_FAN_LED_OFF = "fan_set_led_off" @@ -167,9 +165,6 @@ SERVICE_SET_LEARN_MODE_OFF = "fan_set_learn_mode_off" SERVICE_SET_VOLUME = "fan_set_volume" SERVICE_RESET_FILTER = "fan_reset_filter" SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" -SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" -SERVICE_SET_DRY_ON = "fan_set_dry_on" -SERVICE_SET_DRY_OFF = "fan_set_dry_off" SERVICE_SET_DRY = "set_dry" SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index 90d31765307..4c153292d7e 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -211,49 +211,6 @@ fan_set_extra_features: min: 0 max: 1 -fan_set_target_humidity: - name: Fan set target humidity - description: Set the target humidity. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - humidity: - name: Humidity - description: Target humidity. - required: true - selector: - number: - min: 30 - max: 80 - step: 10 - unit_of_measurement: '%' - -fan_set_dry_on: - name: Fan set dry on - description: Turn the dry mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - -fan_set_dry_off: - name: Fan set dry off - description: Turn the dry mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - fan_set_motor_speed: name: Fan set motor speed description: Set the target motor speed. From cb2103d96cbeb9733db55e5c6834c12360d09a1e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 1 Aug 2021 04:08:39 -0400 Subject: [PATCH 776/818] Fix file path error in nfandroidtv (#53814) --- homeassistant/components/nfandroidtv/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index c2a42760aec..8cc1b0031f7 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -201,8 +201,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - with open(local_path, "rb") as path_handle: - return path_handle + return open(local_path, "rb") # pylint: disable=consider-using-with _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: _LOGGER.warning("Neither URL nor local path found in params!") From ec35b920522a72a6a0bfc9d12809b30e2ca31a5f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Aug 2021 00:06:28 +0200 Subject: [PATCH 777/818] Update frontend to 20210801.0 (#53841) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 02ee134523e..84de9b92c97 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210730.0" + "home-assistant-frontend==20210801.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1418f59fedf..db6ffd1d071 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210730.0 +home-assistant-frontend==20210801.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index a2a24953b71..107a538550c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210730.0 +home-assistant-frontend==20210801.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c934e07675..ea0e298a96b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210730.0 +home-assistant-frontend==20210801.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1019bb059dfe687af603931800b61a848057ddac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Aug 2021 15:07:34 -0700 Subject: [PATCH 778/818] Bumped version to 2021.8.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9a3f47848bf..974fcd01b7b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From e093e0bf10ca30e8f06f5bebc1cb61d06d4ec83b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 2 Aug 2021 03:40:04 +0000 Subject: [PATCH 779/818] [ci skip] Translation update --- .../components/abode/translations/es-419.json | 6 ++- .../accuweather/translations/es-419.json | 5 ++ .../components/adax/translations/cs.json | 19 +++++++ .../components/adax/translations/fr.json | 6 +++ .../components/adax/translations/it.json | 20 ++++++++ .../components/aemet/translations/de.json | 2 +- .../components/aemet/translations/fr.json | 9 ++++ .../airvisual/translations/sensor.fr.json | 13 ++++- .../airvisual/translations/sensor.he.json | 8 +++ .../airvisual/translations/sensor.it.json | 20 ++++++++ .../airvisual/translations/sensor.no.json | 8 +++ .../alarm_control_panel/translations/fr.json | 2 + .../components/ambee/translations/fr.json | 28 +++++++++++ .../ambee/translations/sensor.fr.json | 10 ++++ .../ambee/translations/sensor.he.json | 9 ++++ .../components/arcam_fmj/translations/he.json | 8 ++- .../components/asuswrt/translations/de.json | 2 +- .../components/august/translations/he.json | 2 +- .../components/auth/translations/he.json | 6 +-- .../binary_sensor/translations/he.json | 24 ++++----- .../components/bosch_shc/translations/fr.json | 38 ++++++++++++++ .../components/brother/translations/de.json | 2 +- .../buienradar/translations/fr.json | 6 +++ .../components/calendar/translations/he.json | 2 +- .../components/cast/translations/he.json | 2 +- .../components/climacell/translations/de.json | 2 +- .../cloudflare/translations/cs.json | 6 +++ .../cloudflare/translations/fr.json | 7 +++ .../components/co2signal/translations/cs.json | 30 +++++++++++ .../components/co2signal/translations/fr.json | 8 ++- .../components/co2signal/translations/it.json | 34 +++++++++++++ .../components/coinbase/translations/cs.json | 19 +++++++ .../components/coinbase/translations/de.json | 2 +- .../components/coinbase/translations/fr.json | 41 +++++++++++++++ .../components/coinbase/translations/nl.json | 2 +- .../configurator/translations/he.json | 6 +-- .../conversation/translations/he.json | 2 +- .../coronavirus/translations/fr.json | 3 +- .../components/cover/translations/he.json | 4 +- .../demo/translations/select.fr.json | 9 ++++ .../device_tracker/translations/he.json | 2 +- .../devolo_home_control/translations/cs.json | 3 +- .../devolo_home_control/translations/fr.json | 6 ++- .../devolo_home_control/translations/he.json | 2 +- .../components/directv/translations/he.json | 8 +++ .../components/dsmr/translations/fr.json | 25 +++++++++- .../components/emonitor/translations/he.json | 2 +- .../components/energy/translations/cs.json | 3 ++ .../components/energy/translations/de.json | 3 ++ .../components/energy/translations/fr.json | 3 ++ .../components/energy/translations/he.json | 3 ++ .../components/energy/translations/it.json | 3 ++ .../components/energy/translations/nl.json | 3 ++ .../components/energy/translations/pl.json | 3 ++ .../components/energy/translations/ru.json | 3 ++ .../energy/translations/zh-Hant.json | 3 ++ .../enphase_envoy/translations/fr.json | 3 +- .../components/flipr/translations/cs.json | 20 ++++++++ .../components/flipr/translations/fr.json | 1 + .../components/flipr/translations/he.json | 5 ++ .../components/flipr/translations/it.json | 30 +++++++++++ .../components/flipr/translations/pl.json | 4 +- .../components/flume/translations/fr.json | 3 +- .../forecast_solar/translations/fr.json | 22 +++++++- .../freedompro/translations/fr.json | 11 ++++ .../components/fritz/translations/fr.json | 25 +++++++++- .../garages_amsterdam/translations/fr.json | 18 +++++++ .../components/goalzero/translations/fr.json | 8 ++- .../components/gogogate2/translations/fr.json | 1 + .../components/group/translations/he.json | 4 +- .../growatt_server/translations/cs.json | 11 ++++ .../growatt_server/translations/fr.json | 28 +++++++++++ .../growatt_server/translations/he.json | 5 ++ .../growatt_server/translations/it.json | 1 + .../components/guardian/translations/fr.json | 3 ++ .../components/harmony/translations/he.json | 2 +- .../home_plus_control/translations/de.json | 2 +- .../components/homekit/translations/it.json | 2 +- .../homekit_controller/translations/fr.json | 2 + .../components/honeywell/translations/cs.json | 15 ++++++ .../components/honeywell/translations/fr.json | 10 +++- .../components/honeywell/translations/it.json | 17 +++++++ .../huawei_lte/translations/fr.json | 3 +- .../components/hue/translations/fr.json | 4 +- .../translations/fr.json | 1 + .../translations/he.json | 2 +- .../input_boolean/translations/he.json | 2 +- .../components/isy994/translations/fr.json | 8 +++ .../keenetic_ndms2/translations/fr.json | 5 +- .../components/kodi/translations/he.json | 2 +- .../components/konnected/translations/he.json | 16 ++++++ .../components/kraken/translations/fr.json | 30 +++++++++++ .../components/kraken/translations/he.json | 21 ++++++++ .../components/litejet/translations/fr.json | 10 ++++ .../components/litejet/translations/it.json | 10 ++++ .../components/lyric/translations/fr.json | 6 ++- .../meteoclimatic/translations/fr.json | 20 ++++++++ .../components/mikrotik/translations/de.json | 6 +-- .../modern_forms/translations/fr.json | 28 +++++++++++ .../components/motioneye/translations/fr.json | 15 ++++-- .../components/myq/translations/fr.json | 3 +- .../components/mysensors/translations/de.json | 2 +- .../components/nam/translations/fr.json | 4 ++ .../components/nest/translations/he.json | 2 +- .../components/nexia/translations/fr.json | 1 + .../nfandroidtv/translations/cs.json | 19 +++++++ .../nfandroidtv/translations/fr.json | 21 ++++++++ .../nfandroidtv/translations/it.json | 21 ++++++++ .../nfandroidtv/translations/nl.json | 1 + .../nfandroidtv/translations/pl.json | 2 +- .../nmap_tracker/translations/cs.json | 7 +++ .../nmap_tracker/translations/fr.json | 32 +++++++++++- .../components/onvif/translations/fr.json | 13 +++++ .../components/onvif/translations/he.json | 2 +- .../ovo_energy/translations/he.json | 3 +- .../philips_js/translations/fr.json | 9 ++++ .../components/picnic/translations/fr.json | 5 ++ .../components/plugwise/translations/he.json | 3 ++ .../components/poolsense/translations/de.json | 2 +- .../components/prosegur/translations/ca.json | 29 +++++++++++ .../components/prosegur/translations/cs.json | 27 ++++++++++ .../components/prosegur/translations/de.json | 29 +++++++++++ .../components/prosegur/translations/et.json | 29 +++++++++++ .../components/prosegur/translations/fr.json | 29 +++++++++++ .../components/prosegur/translations/he.json | 28 +++++++++++ .../components/prosegur/translations/it.json | 29 +++++++++++ .../components/prosegur/translations/nl.json | 29 +++++++++++ .../components/prosegur/translations/no.json | 18 +++++++ .../components/prosegur/translations/pl.json | 29 +++++++++++ .../components/prosegur/translations/ru.json | 29 +++++++++++ .../prosegur/translations/zh-Hant.json | 29 +++++++++++ .../components/ps4/translations/he.json | 4 +- .../pvpc_hourly_pricing/translations/de.json | 2 +- .../pvpc_hourly_pricing/translations/fr.json | 15 ++++++ .../pvpc_hourly_pricing/translations/he.json | 7 +++ .../components/renault/translations/ca.json | 27 ++++++++++ .../components/renault/translations/cs.json | 18 +++++++ .../components/renault/translations/de.json | 27 ++++++++++ .../components/renault/translations/en.json | 44 ++++++++-------- .../components/renault/translations/et.json | 27 ++++++++++ .../components/renault/translations/fr.json | 27 ++++++++++ .../components/renault/translations/he.json | 18 +++++++ .../components/renault/translations/it.json | 27 ++++++++++ .../components/renault/translations/nl.json | 27 ++++++++++ .../components/renault/translations/no.json | 12 +++++ .../components/renault/translations/pl.json | 27 ++++++++++ .../components/renault/translations/ru.json | 27 ++++++++++ .../renault/translations/zh-Hant.json | 27 ++++++++++ .../components/roku/translations/he.json | 8 +++ .../components/roomba/translations/he.json | 2 +- .../components/roon/translations/nl.json | 2 +- .../components/samsungtv/translations/fr.json | 11 +++- .../screenlogic/translations/de.json | 2 +- .../components/select/translations/fr.json | 11 ++++ .../components/sia/translations/fr.json | 50 +++++++++++++++++++ .../simplisafe/translations/de.json | 2 +- .../components/sma/translations/fr.json | 12 +++-- .../smartthings/translations/he.json | 2 +- .../components/smarttub/translations/fr.json | 3 +- .../somfy_mylink/translations/de.json | 2 +- .../components/sonos/translations/fr.json | 1 + .../components/sonos/translations/he.json | 4 +- .../switcher_kis/translations/fr.json | 3 +- .../switcher_kis/translations/it.json | 13 +++++ .../components/syncthing/translations/fr.json | 22 ++++++++ .../synology_dsm/translations/cs.json | 10 +++- .../synology_dsm/translations/fr.json | 6 ++- .../synology_dsm/translations/he.json | 11 +++- .../synology_dsm/translations/it.json | 11 +++- .../system_bridge/translations/fr.json | 9 +++- .../system_health/translations/he.json | 2 +- .../components/tesla/translations/ca.json | 1 + .../components/tesla/translations/de.json | 1 + .../components/tesla/translations/et.json | 1 + .../components/tesla/translations/fr.json | 1 + .../components/tesla/translations/it.json | 1 + .../components/tesla/translations/nl.json | 1 + .../components/tesla/translations/pl.json | 1 + .../components/tesla/translations/ru.json | 1 + .../tesla/translations/zh-Hant.json | 1 + .../totalconnect/translations/fr.json | 3 +- .../components/tplink/translations/he.json | 4 +- .../components/traccar/translations/de.json | 2 +- .../components/unifi/translations/he.json | 8 +++ .../components/upb/translations/de.json | 2 +- .../components/upnp/translations/fr.json | 9 ++++ .../components/upnp/translations/he.json | 6 +++ .../components/wallbox/translations/fr.json | 22 ++++++++ .../components/wemo/translations/fr.json | 5 ++ .../components/wled/translations/fr.json | 9 ++++ .../wolflink/translations/sensor.de.json | 2 +- .../wolflink/translations/sensor.he.json | 1 + .../xiaomi_miio/translations/de.json | 2 +- .../xiaomi_miio/translations/fr.json | 36 +++++++++++-- .../xiaomi_miio/translations/he.json | 2 +- .../yale_smart_alarm/translations/ca.json | 8 +-- .../yale_smart_alarm/translations/cs.json | 28 +++++++++++ .../yale_smart_alarm/translations/en.json | 8 +-- .../yale_smart_alarm/translations/fr.json | 28 +++++++++++ .../yale_smart_alarm/translations/he.json | 28 +++++++++++ .../yale_smart_alarm/translations/it.json | 28 +++++++++++ .../yale_smart_alarm/translations/nl.json | 28 +++++++++++ .../yale_smart_alarm/translations/no.json | 20 ++++++++ .../yale_smart_alarm/translations/pl.json | 28 +++++++++++ .../yale_smart_alarm/translations/ru.json | 8 +-- .../translations/zh-Hant.json | 8 +-- .../yamaha_musiccast/translations/fr.json | 23 +++++++++ .../components/yeelight/translations/fr.json | 4 ++ .../components/youless/translations/ca.json | 15 ++++++ .../components/youless/translations/cs.json | 15 ++++++ .../components/youless/translations/de.json | 15 ++++++ .../components/youless/translations/en.json | 12 ++--- .../components/youless/translations/et.json | 15 ++++++ .../components/youless/translations/fr.json | 15 ++++++ .../components/youless/translations/he.json | 15 ++++++ .../components/youless/translations/it.json | 15 ++++++ .../components/youless/translations/nl.json | 15 ++++++ .../components/youless/translations/no.json | 11 ++++ .../components/youless/translations/pl.json | 15 ++++++ .../components/youless/translations/ru.json | 15 ++++++ .../youless/translations/zh-Hant.json | 15 ++++++ .../components/zha/translations/fr.json | 2 + .../components/zwave/translations/de.json | 2 +- .../components/zwave/translations/he.json | 6 +-- .../components/zwave_js/translations/de.json | 2 +- .../components/zwave_js/translations/fr.json | 48 +++++++++++++++++- 226 files changed, 2440 insertions(+), 174 deletions(-) create mode 100644 homeassistant/components/adax/translations/cs.json create mode 100644 homeassistant/components/adax/translations/it.json create mode 100644 homeassistant/components/airvisual/translations/sensor.he.json create mode 100644 homeassistant/components/airvisual/translations/sensor.it.json create mode 100644 homeassistant/components/airvisual/translations/sensor.no.json create mode 100644 homeassistant/components/ambee/translations/fr.json create mode 100644 homeassistant/components/ambee/translations/sensor.fr.json create mode 100644 homeassistant/components/ambee/translations/sensor.he.json create mode 100644 homeassistant/components/bosch_shc/translations/fr.json create mode 100644 homeassistant/components/co2signal/translations/cs.json create mode 100644 homeassistant/components/co2signal/translations/it.json create mode 100644 homeassistant/components/coinbase/translations/cs.json create mode 100644 homeassistant/components/coinbase/translations/fr.json create mode 100644 homeassistant/components/demo/translations/select.fr.json create mode 100644 homeassistant/components/energy/translations/cs.json create mode 100644 homeassistant/components/energy/translations/de.json create mode 100644 homeassistant/components/energy/translations/fr.json create mode 100644 homeassistant/components/energy/translations/he.json create mode 100644 homeassistant/components/energy/translations/it.json create mode 100644 homeassistant/components/energy/translations/nl.json create mode 100644 homeassistant/components/energy/translations/pl.json create mode 100644 homeassistant/components/energy/translations/ru.json create mode 100644 homeassistant/components/energy/translations/zh-Hant.json create mode 100644 homeassistant/components/flipr/translations/cs.json create mode 100644 homeassistant/components/flipr/translations/it.json create mode 100644 homeassistant/components/garages_amsterdam/translations/fr.json create mode 100644 homeassistant/components/growatt_server/translations/cs.json create mode 100644 homeassistant/components/growatt_server/translations/fr.json create mode 100644 homeassistant/components/honeywell/translations/cs.json create mode 100644 homeassistant/components/honeywell/translations/it.json create mode 100644 homeassistant/components/kraken/translations/fr.json create mode 100644 homeassistant/components/meteoclimatic/translations/fr.json create mode 100644 homeassistant/components/modern_forms/translations/fr.json create mode 100644 homeassistant/components/nfandroidtv/translations/cs.json create mode 100644 homeassistant/components/nfandroidtv/translations/fr.json create mode 100644 homeassistant/components/nfandroidtv/translations/it.json create mode 100644 homeassistant/components/nmap_tracker/translations/cs.json create mode 100644 homeassistant/components/prosegur/translations/ca.json create mode 100644 homeassistant/components/prosegur/translations/cs.json create mode 100644 homeassistant/components/prosegur/translations/de.json create mode 100644 homeassistant/components/prosegur/translations/et.json create mode 100644 homeassistant/components/prosegur/translations/fr.json create mode 100644 homeassistant/components/prosegur/translations/he.json create mode 100644 homeassistant/components/prosegur/translations/it.json create mode 100644 homeassistant/components/prosegur/translations/nl.json create mode 100644 homeassistant/components/prosegur/translations/no.json create mode 100644 homeassistant/components/prosegur/translations/pl.json create mode 100644 homeassistant/components/prosegur/translations/ru.json create mode 100644 homeassistant/components/prosegur/translations/zh-Hant.json create mode 100644 homeassistant/components/renault/translations/ca.json create mode 100644 homeassistant/components/renault/translations/cs.json create mode 100644 homeassistant/components/renault/translations/de.json create mode 100644 homeassistant/components/renault/translations/et.json create mode 100644 homeassistant/components/renault/translations/fr.json create mode 100644 homeassistant/components/renault/translations/he.json create mode 100644 homeassistant/components/renault/translations/it.json create mode 100644 homeassistant/components/renault/translations/nl.json create mode 100644 homeassistant/components/renault/translations/no.json create mode 100644 homeassistant/components/renault/translations/pl.json create mode 100644 homeassistant/components/renault/translations/ru.json create mode 100644 homeassistant/components/renault/translations/zh-Hant.json create mode 100644 homeassistant/components/sia/translations/fr.json create mode 100644 homeassistant/components/switcher_kis/translations/it.json create mode 100644 homeassistant/components/syncthing/translations/fr.json create mode 100644 homeassistant/components/wallbox/translations/fr.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/cs.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/fr.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/he.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/it.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/nl.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/no.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/pl.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/fr.json create mode 100644 homeassistant/components/youless/translations/ca.json create mode 100644 homeassistant/components/youless/translations/cs.json create mode 100644 homeassistant/components/youless/translations/de.json create mode 100644 homeassistant/components/youless/translations/et.json create mode 100644 homeassistant/components/youless/translations/fr.json create mode 100644 homeassistant/components/youless/translations/he.json create mode 100644 homeassistant/components/youless/translations/it.json create mode 100644 homeassistant/components/youless/translations/nl.json create mode 100644 homeassistant/components/youless/translations/no.json create mode 100644 homeassistant/components/youless/translations/pl.json create mode 100644 homeassistant/components/youless/translations/ru.json create mode 100644 homeassistant/components/youless/translations/zh-Hant.json diff --git a/homeassistant/components/abode/translations/es-419.json b/homeassistant/components/abode/translations/es-419.json index 9de6d9d185a..6d380e5bb43 100644 --- a/homeassistant/components/abode/translations/es-419.json +++ b/homeassistant/components/abode/translations/es-419.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "reauth_successful": "La reautenticaci\u00f3n fue exitosa", "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" }, "step": { @@ -15,7 +18,8 @@ }, "reauth_confirm": { "data": { - "password": "Contrase\u00f1a" + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" }, "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" }, diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json index 92d5d5ef2c2..72d295da073 100644 --- a/homeassistant/components/accuweather/translations/es-419.json +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave de API no v\u00e1lida", "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API." }, "step": { diff --git a/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json new file mode 100644 index 00000000000..ce5fa77543f --- /dev/null +++ b/homeassistant/components/adax/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index a8f036f6ed5..80164e30b54 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, "step": { "user": { "data": { + "account_id": "identifiant de compte", + "host": "H\u00f4te", "password": "Mot de passe" } } diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json new file mode 100644 index 00000000000..c0ccf6aff05 --- /dev/null +++ b/homeassistant/components/adax/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "account_id": "ID account", + "host": "Host", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json index 2a4a927b90a..0704e7d71ba 100644 --- a/homeassistant/components/aemet/translations/de.json +++ b/homeassistant/components/aemet/translations/de.json @@ -15,7 +15,7 @@ "name": "Name der Integration" }, "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "[void]" + "title": "AEMET OpenData" } } }, diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index bb1e792aa5e..4ad76320f03 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Recueillir les donn\u00e9es des stations m\u00e9t\u00e9orologiques AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.fr.json b/homeassistant/components/airvisual/translations/sensor.fr.json index b3018d53bc2..3050d6fb158 100644 --- a/homeassistant/components/airvisual/translations/sensor.fr.json +++ b/homeassistant/components/airvisual/translations/sensor.fr.json @@ -1,11 +1,20 @@ { "state": { "airvisual__pollutant_label": { - "co": "Monoxyde de carbone" + "co": "Monoxyde de carbone", + "n2": "Dioxyde d'azote", + "o3": "Ozone", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Dioxyde de soufre" }, "airvisual__pollutant_level": { "good": "Bon", - "hazardous": "Hasardeux" + "hazardous": "Hasardeux", + "moderate": "Mod\u00e9rer", + "unhealthy": "Malsain", + "unhealthy_sensitive": "Malsain pour les groupes sensibles", + "very_unhealthy": "Tr\u00e8s malsain" } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.he.json b/homeassistant/components/airvisual/translations/sensor.he.json new file mode 100644 index 00000000000..28ac8c5c3e4 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.he.json @@ -0,0 +1,8 @@ +{ + "state": { + "airvisual__pollutant_level": { + "good": "\u05d8\u05d5\u05d1", + "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.it.json b/homeassistant/components/airvisual/translations/sensor.it.json new file mode 100644 index 00000000000..7fb8b98215c --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.it.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Monossido di carbonio", + "n2": "Anidride nitrosa", + "o3": "Ozono", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Anidride solforosa" + }, + "airvisual__pollutant_level": { + "good": "Buono", + "hazardous": "Pericoloso", + "moderate": "Moderato", + "unhealthy": "Malsano", + "unhealthy_sensitive": "Malsano per gruppi sensibili", + "very_unhealthy": "Molto malsano" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.no.json b/homeassistant/components/airvisual/translations/sensor.no.json new file mode 100644 index 00000000000..86c95f8e8f2 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "airvisual__pollutant_label": { + "p1": "PM10", + "p2": "PM2.5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index c7e010e805e..6d8ee9c08c3 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -4,6 +4,7 @@ "arm_away": "Armer {entity_name} en mode \"sortie\"", "arm_home": "Armer {entity_name} en mode \"maison\"", "arm_night": "Armer {entity_name} en mode \"nuit\"", + "arm_vacation": "Armer {entity_name} vacances", "disarm": "D\u00e9sarmer {entity_name}", "trigger": "D\u00e9clencheur {entity_name}" }, @@ -29,6 +30,7 @@ "armed_custom_bypass": "Arm\u00e9 avec exception personnalis\u00e9e", "armed_home": "Enclench\u00e9e (pr\u00e9sent)", "armed_night": "Enclench\u00e9 (nuit)", + "armed_vacation": "Arm\u00e9es vacances", "arming": "Activation", "disarmed": "D\u00e9sactiv\u00e9e", "disarming": "D\u00e9sactivation", diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json new file mode 100644 index 00000000000..bbb09edf763 --- /dev/null +++ b/homeassistant/components/ambee/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 API non valide" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "cl\u00e9 API", + "description": "R\u00e9-authentifiez-vous avec votre compte Ambee." + } + }, + "user": { + "data": { + "api_key": "cl\u00e9 API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + }, + "description": "Configurer Ambee pour l'int\u00e9grer \u00e0 Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.fr.json b/homeassistant/components/ambee/translations/sensor.fr.json new file mode 100644 index 00000000000..76dc3fe6301 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.fr.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Haute", + "low": "Faible", + "moderate": "Mod\u00e9rer", + "very high": "Tr\u00e8s \u00e9lev\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.he.json b/homeassistant/components/ambee/translations/sensor.he.json new file mode 100644 index 00000000000..14ae06f2bc9 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.he.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "high": "\u05d2\u05d1\u05d5\u05d4", + "low": "\u05e0\u05de\u05d5\u05da", + "very high": "\u05d2\u05d1\u05d5\u05d4 \u05de\u05d0\u05d5\u05d3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/he.json b/homeassistant/components/arcam_fmj/translations/he.json index 0a4bd9ca12a..c07b9af0c67 100644 --- a/homeassistant/components/arcam_fmj/translations/he.json +++ b/homeassistant/components/arcam_fmj/translations/he.json @@ -5,10 +5,16 @@ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "error": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "flow_title": "{host}", "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Arcam FMJ \u05d1- '{host}' \u05dc-Home Assistant?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Arcam FMJ \u05d1-`{host}` \u05dc-Home Assistant?" }, "user": { "data": { diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index fcd2157d321..6a860311a32 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -24,7 +24,7 @@ "username": "Benutzername" }, "description": "Einstellen der erforderlichen Parameter f\u00fcr die Verbindung mit deinem Router.", - "title": "" + "title": "AsusWRT" } } }, diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json index 5fb689b2562..aeb3c6a9f14 100644 --- a/homeassistant/components/august/translations/he.json +++ b/homeassistant/components/august/translations/he.json @@ -25,7 +25,7 @@ } }, "validation": { - "description": "\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea {login_method} \u05e9\u05dc\u05da ({\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9}) \u05d5\u05d4\u05d6\u05df \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05d4\u05dc\u05df" + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea {login_method} ( {username} ) \u05d5\u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05d4\u05dc\u05df" } } } diff --git a/homeassistant/components/auth/translations/he.json b/homeassistant/components/auth/translations/he.json index bc1826d4d79..6bbf472a14b 100644 --- a/homeassistant/components/auth/translations/he.json +++ b/homeassistant/components/auth/translations/he.json @@ -2,18 +2,18 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd." + "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 \u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05d6\u05de\u05d9\u05e0\u05d9\u05dd." }, "error": { "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." }, "step": { "init": { - "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify", + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05d7\u05d3 \u05de\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 \u05d4\u05d4\u05d5\u05d3\u05e2\u05d5\u05ea:", "title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify" }, "setup": { - "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:", + "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea ** Notify. {notify_service} **. \u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05d5\u05ea\u05d5 \u05dc\u05de\u05d8\u05d4:", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4" } }, diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 31a75c3acdf..c345b1a94ce 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -35,19 +35,19 @@ "on": "\u05de\u05d7\u05d5\u05d1\u05e8" }, "door": { - "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", + "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "garage_door": { - "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", - "on": "\u05e4\u05ea\u05d5\u05d7\u05d4" + "off": "\u05e1\u05d2\u05d5\u05e8", + "on": "\u05e4\u05ea\u05d5\u05d7" }, "gas": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "heat": { - "off": "\u05e8\u05d2\u05d9\u05dc", + "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", "on": "\u05d7\u05dd" }, "light": { @@ -64,7 +64,7 @@ }, "motion": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "moving": { "off": "\u05dc\u05d0 \u05d6\u05d6", @@ -72,10 +72,10 @@ }, "occupancy": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "opening": { - "off": "\u05e0\u05e1\u05d2\u05e8", + "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "plug": { @@ -95,18 +95,18 @@ }, "smoke": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "sound": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "vibration": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "window": { - "off": "\u05e0\u05e1\u05d2\u05e8", + "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" } }, diff --git a/homeassistant/components/bosch_shc/translations/fr.json b/homeassistant/components/bosch_shc/translations/fr.json new file mode 100644 index 00000000000..38a48b269b4 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "pairing_failed": "L'appairage a \u00e9chou\u00e9\u00a0; veuillez v\u00e9rifier que le Bosch Smart Home Controller est en mode d'appairage (voyant clignotant) et que votre mot de passe est correct.", + "session_error": "Erreur de session\u00a0: l'API renvoie un r\u00e9sultat non-OK.", + "unknown": "Erreur inattendue" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Veuillez appuyer sur le bouton situ\u00e9 \u00e0 l'avant du Bosch Smart Home Controller jusqu'\u00e0 ce que le voyant commence \u00e0 clignoter.\n Pr\u00eat \u00e0 continuer \u00e0 configurer {model} @ {host} avec Home Assistant\u00a0?" + }, + "credentials": { + "data": { + "password": "Mot de passe du contr\u00f4leur Smart Home" + } + }, + "reauth_confirm": { + "description": "L'int\u00e9gration bosch_shc doit r\u00e9-authentifier votre compte", + "title": "R\u00e9authentification de l'int\u00e9gration" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurez votre Bosch Smart Home Controller pour permettre la surveillance et le contr\u00f4le avec Home Assistant.", + "title": "Param\u00e8tres d'authentification SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index b79ff0a7619..8126a04f21d 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", - "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" + "wrong_host": "Ung\u00fcltiger Hostname oder IP-Adresse" }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/buienradar/translations/fr.json b/homeassistant/components/buienradar/translations/fr.json index d9c2fadcbf7..19b7737ae11 100644 --- a/homeassistant/components/buienradar/translations/fr.json +++ b/homeassistant/components/buienradar/translations/fr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/calendar/translations/he.json b/homeassistant/components/calendar/translations/he.json index c80aaab5c1e..9a633670698 100644 --- a/homeassistant/components/calendar/translations/he.json +++ b/homeassistant/components/calendar/translations/he.json @@ -5,5 +5,5 @@ "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, - "title": "\u05dc\u05d5\u05bc\u05d7\u05b7 \u05e9\u05c1\u05b8\u05e0\u05b8\u05d4" + "title": "\u05dc\u05d5\u05d7 \u05e9\u05e0\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index 3a9552980e6..d50e5b20684 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Google Cast \u05e0\u05d7\u05d5\u05e6\u05d4." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd." diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index 8e269db785b..123a1257d99 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -25,7 +25,7 @@ "data": { "timestep": "Minuten zwischen den Kurzvorhersagen" }, - "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", + "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", "title": "Aktualisiere ClimaCell-Optionen" } } diff --git a/homeassistant/components/cloudflare/translations/cs.json b/homeassistant/components/cloudflare/translations/cs.json index e20f26236be..8f88377860b 100644 --- a/homeassistant/components/cloudflare/translations/cs.json +++ b/homeassistant/components/cloudflare/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, @@ -11,6 +12,11 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API token" + } + }, "records": { "data": { "records": "Z\u00e1znamy" diff --git a/homeassistant/components/cloudflare/translations/fr.json b/homeassistant/components/cloudflare/translations/fr.json index be6d4c3e2b3..677dc8552fb 100644 --- a/homeassistant/components/cloudflare/translations/fr.json +++ b/homeassistant/components/cloudflare/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "R\u00e9-authentification r\u00e9ussie", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "unknown": "Erreur inattendue" }, @@ -11,6 +12,12 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "Jeton API", + "description": "R\u00e9-authentifiez-vous avec votre compte Cloudflare." + } + }, "records": { "data": { "records": "Enregistrements" diff --git a/homeassistant/components/co2signal/translations/cs.json b/homeassistant/components/co2signal/translations/cs.json new file mode 100644 index 00000000000..954168d1ee2 --- /dev/null +++ b/homeassistant/components/co2signal/translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + }, + "country": { + "data": { + "country_code": "K\u00f3d zem\u011b" + } + }, + "user": { + "data": { + "api_key": "P\u0159\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/fr.json b/homeassistant/components/co2signal/translations/fr.json index 549124674dd..4b36bd3bd74 100644 --- a/homeassistant/components/co2signal/translations/fr.json +++ b/homeassistant/components/co2signal/translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "api_ratelimit": "Limite de d\u00e9bit de l\u2019API d\u00e9pass\u00e9e", "unknown": "Erreur inattendue" }, "error": { @@ -22,8 +24,10 @@ }, "user": { "data": { - "api_key": "Token d'acc\u00e8s" - } + "api_key": "Token d'acc\u00e8s", + "location": "Obtenir des donn\u00e9es pour" + }, + "description": "Visitez https://co2signal.com/ pour demander un jeton." } } } diff --git a/homeassistant/components/co2signal/translations/it.json b/homeassistant/components/co2signal/translations/it.json new file mode 100644 index 00000000000..0db63a1e912 --- /dev/null +++ b/homeassistant/components/co2signal/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "api_ratelimit": "Limite di frequenza API superato", + "unknown": "Errore imprevisto" + }, + "error": { + "api_ratelimit": "Limite di frequenza API superato", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine" + } + }, + "country": { + "data": { + "country_code": "Prefisso internazionale" + } + }, + "user": { + "data": { + "api_key": "Token di accesso", + "location": "Ottieni dati per" + }, + "description": "Visita https://co2signal.com/ per richiedere un token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json new file mode 100644 index 00000000000..24dc9ec4e14 --- /dev/null +++ b/homeassistant/components/coinbase/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 25d20fe8cf2..45360acd288 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -16,7 +16,7 @@ "currencies": "Kontostand W\u00e4hrungen", "exchange_rates": "Wechselkurse" }, - "description": "Bitte gib die Details deines API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt. Trenne mehrere W\u00e4hrungen mit einem Komma (z. B. \"BTC, EUR\")", + "description": "Bitte gib die Details deines API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt.", "title": "Coinbase API Schl\u00fcssel Details" } } diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json new file mode 100644 index 00000000000..e0ec1ae200d --- /dev/null +++ b/homeassistant/components/coinbase/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "cl\u00e9 API", + "api_token": "API secr\u00e8te", + "currencies": "Devises du solde du compte", + "exchange_rates": "Taux d'\u00e9change" + }, + "description": "Veuillez saisir les d\u00e9tails de votre cl\u00e9 API tels que fournis par Coinbase.", + "title": "D\u00e9tails de la cl\u00e9 de l'API Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Un ou plusieurs des soldes de devises demand\u00e9s ne sont pas fournis par votre API Coinbase.", + "exchange_rate_unavaliable": "Un ou plusieurs des taux de change demand\u00e9s ne sont pas fournis par Coinbase.", + "unknown": "Erreur inattendue" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Soldes du portefeuille \u00e0 d\u00e9clarer.", + "exchange_base": "Devise de base pour les capteurs de taux de change.", + "exchange_rate_currencies": "Taux de change \u00e0 d\u00e9clarer." + }, + "description": "Ajuster les options de Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index b13caef1976..e277eaf67db 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -16,7 +16,7 @@ "currencies": "Valuta's van rekeningsaldo", "exchange_rates": "Wisselkoersen" }, - "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase. Scheidt meerdere valuta's met een komma (bijv. \"BTC, EUR\")", + "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase.", "title": "Coinbase API Sleutel Details" } } diff --git a/homeassistant/components/configurator/translations/he.json b/homeassistant/components/configurator/translations/he.json index 7cc7aad41d7..aeff95ca5ce 100644 --- a/homeassistant/components/configurator/translations/he.json +++ b/homeassistant/components/configurator/translations/he.json @@ -1,9 +1,9 @@ { "state": { "_": { - "configure": "\u05d4\u05d2\u05d3\u05e8", - "configured": "\u05d4\u05d5\u05d2\u05d3\u05e8" + "configure": "\u05d4\u05d2\u05d3\u05e8\u05d4", + "configured": "\u05de\u05d5\u05d2\u05d3\u05e8" } }, - "title": "\u05e7\u05d5\u05e0\u05e4\u05d9\u05d2\u05d5\u05e8\u05d8\u05d5\u05e8" + "title": "\u05e7\u05d5\u05d1\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/conversation/translations/he.json b/homeassistant/components/conversation/translations/he.json index eeccec319af..63cfb10abe8 100644 --- a/homeassistant/components/conversation/translations/he.json +++ b/homeassistant/components/conversation/translations/he.json @@ -1,3 +1,3 @@ { - "title": "\u05e9\u05c2\u05b4\u05d9\u05d7\u05b8\u05d4" + "title": "\u05e9\u05d9\u05d7\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/fr.json b/homeassistant/components/coronavirus/translations/fr.json index 21a72d80f61..9a9a960cf31 100644 --- a/homeassistant/components/coronavirus/translations/fr.json +++ b/homeassistant/components/coronavirus/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ce pays est d\u00e9j\u00e0 configur\u00e9." + "already_configured": "Ce pays est d\u00e9j\u00e0 configur\u00e9.", + "cannot_connect": "\u00c9chec de connexion" }, "step": { "user": { diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json index fce73cc1698..5dad593467c 100644 --- a/homeassistant/components/cover/translations/he.json +++ b/homeassistant/components/cover/translations/he.json @@ -6,11 +6,11 @@ }, "state": { "_": { - "closed": "\u05e0\u05e1\u05d2\u05e8", + "closed": "\u05e1\u05d2\u05d5\u05e8", "closing": "\u05e1\u05d5\u05d2\u05e8", "open": "\u05e4\u05ea\u05d5\u05d7", "opening": "\u05e4\u05d5\u05ea\u05d7", - "stopped": "\u05e2\u05e6\u05d5\u05e8" + "stopped": "\u05e2\u05e6\u05e8" } }, "title": "\u05d5\u05d9\u05dc\u05d5\u05df" diff --git a/homeassistant/components/demo/translations/select.fr.json b/homeassistant/components/demo/translations/select.fr.json new file mode 100644 index 00000000000..d2b214e4078 --- /dev/null +++ b/homeassistant/components/demo/translations/select.fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Vitesse de la lumi\u00e8re", + "ludicrous_speed": "Vitesse ridicule", + "ridiculous_speed": "Vitesse ridicule" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/he.json b/homeassistant/components/device_tracker/translations/he.json index 5db22ed4071..2f3ccc1ec1e 100644 --- a/homeassistant/components/device_tracker/translations/he.json +++ b/homeassistant/components/device_tracker/translations/he.json @@ -5,5 +5,5 @@ "not_home": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea" } }, - "title": "\u05de\u05e2\u05e7\u05d1 \u05de\u05db\u05e9\u05d9\u05e8" + "title": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/cs.json b/homeassistant/components/devolo_home_control/translations/cs.json index 44906c51207..54169346968 100644 --- a/homeassistant/components/devolo_home_control/translations/cs.json +++ b/homeassistant/components/devolo_home_control/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index d0f806c042e..bc9a5715238 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Cette unit\u00e9 centrale Home Control est d\u00e9j\u00e0 utilis\u00e9e." + "already_configured": "Cette unit\u00e9 centrale Home Control est d\u00e9j\u00e0 utilis\u00e9e.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "reauth_failed": "Veuillez utiliser le m\u00eame utilisateur mydevolo que pr\u00e9c\u00e9demment." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index f9ab82881a1..2ac09df14fd 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9/ \u05de\u05d6\u05d4\u05d4 devolo" + "username": "\u05d3\u05d5\u05d0\"\u05dc / \u05de\u05d6\u05d4\u05d4 devolo" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/directv/translations/he.json b/homeassistant/components/directv/translations/he.json index f057c4e4629..bc28ff4eba5 100644 --- a/homeassistant/components/directv/translations/he.json +++ b/homeassistant/components/directv/translations/he.json @@ -9,6 +9,14 @@ }, "flow_title": "{name}", "step": { + "ssdp_confirm": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json index 3f462c74c05..0f348acdbff 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -1,23 +1,46 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_communicate": "\u00c9chec de la communication", + "cannot_connect": "\u00c9chec de connexion" }, "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_communicate": "\u00c9chec de la communication", + "cannot_connect": "\u00c9chec de connexion", "one": "Vide", "other": "Vide" }, "step": { "one": "", "other": "Autre", + "setup_network": { + "data": { + "dsmr_version": "S\u00e9lectionner la version DSMR", + "host": "H\u00f4te", + "port": "Port" + }, + "title": "S\u00e9lectionner l'adresse de connexion" + }, "setup_serial": { "data": { + "dsmr_version": "S\u00e9lectionner la version DSMR", "port": "S\u00e9lectionner un appareil" }, "title": "Appareil" }, "setup_serial_manual_path": { + "data": { + "port": "Chemin du p\u00e9riph\u00e9rique USB" + }, "title": "Chemin" + }, + "user": { + "data": { + "type": "Type de connexion" + }, + "title": "S\u00e9lectionner le type de connexion" } } }, diff --git a/homeassistant/components/emonitor/translations/he.json b/homeassistant/components/emonitor/translations/he.json index 4ec15aa12cb..77bd85b18b8 100644 --- a/homeassistant/components/emonitor/translations/he.json +++ b/homeassistant/components/emonitor/translations/he.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/energy/translations/cs.json b/homeassistant/components/energy/translations/cs.json new file mode 100644 index 00000000000..53457a69447 --- /dev/null +++ b/homeassistant/components/energy/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "Energie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/de.json b/homeassistant/components/energy/translations/de.json new file mode 100644 index 00000000000..53457a69447 --- /dev/null +++ b/homeassistant/components/energy/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Energie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/fr.json b/homeassistant/components/energy/translations/fr.json new file mode 100644 index 00000000000..f947a07baec --- /dev/null +++ b/homeassistant/components/energy/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "\u00c9nergie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/he.json b/homeassistant/components/energy/translations/he.json new file mode 100644 index 00000000000..3c61aad6089 --- /dev/null +++ b/homeassistant/components/energy/translations/he.json @@ -0,0 +1,3 @@ +{ + "title": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/it.json b/homeassistant/components/energy/translations/it.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json new file mode 100644 index 00000000000..53457a69447 --- /dev/null +++ b/homeassistant/components/energy/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Energie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/pl.json b/homeassistant/components/energy/translations/pl.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/ru.json b/homeassistant/components/energy/translations/ru.json new file mode 100644 index 00000000000..b351e407168 --- /dev/null +++ b/homeassistant/components/energy/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u042d\u043d\u0435\u0440\u0433\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/zh-Hant.json b/homeassistant/components/energy/translations/zh-Hant.json new file mode 100644 index 00000000000..bae50fae66e --- /dev/null +++ b/homeassistant/components/energy/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u80fd\u6e90" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json index be1d5f3bca3..9587739e88a 100644 --- a/homeassistant/components/enphase_envoy/translations/fr.json +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/flipr/translations/cs.json b/homeassistant/components/flipr/translations/cs.json new file mode 100644 index 00000000000..29c2ebc1713 --- /dev/null +++ b/homeassistant/components/flipr/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/fr.json b/homeassistant/components/flipr/translations/fr.json index 32769aab9b7..ec9260aa8a7 100644 --- a/homeassistant/components/flipr/translations/fr.json +++ b/homeassistant/components/flipr/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "no_flipr_id_found": "Aucun identifiant Flipr n'est associ\u00e9 \u00e0 votre compte pour le moment. Vous devez d'abord v\u00e9rifier qu'il fonctionne avec l'application mobile de Flipr.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/flipr/translations/he.json b/homeassistant/components/flipr/translations/he.json index 85872f14f2c..ecb8a74bc6f 100644 --- a/homeassistant/components/flipr/translations/he.json +++ b/homeassistant/components/flipr/translations/he.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/flipr/translations/it.json b/homeassistant/components/flipr/translations/it.json new file mode 100644 index 00000000000..399d4c6b9d3 --- /dev/null +++ b/homeassistant/components/flipr/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "no_flipr_id_found": "Nessun ID flipr associato al tuo account per ora. Dovresti prima verificare che funzioni con l'app mobile di Flipr.", + "unknown": "Errore imprevisto" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "ID Flipr" + }, + "description": "Scegli il tuo ID Flipr nell'elenco", + "title": "Scegli il tuo Flipr" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "description": "Connettiti usando il tuo account Flipr.", + "title": "Connettiti a Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/pl.json b/homeassistant/components/flipr/translations/pl.json index 1095c33a83e..436061f7f61 100644 --- a/homeassistant/components/flipr/translations/pl.json +++ b/homeassistant/components/flipr/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", - "no_flipr_id_found": "Brak identyfikatora flipr powi\u0105zanego z Twoim kontem. Sprawd\u017a najpierw, czy dzia\u0142a z aplikacj\u0105 mobiln\u0105 Flipr.", + "no_flipr_id_found": "Brak identyfikatora Flipr powi\u0105zanego z Twoim kontem. Sprawd\u017a najpierw, czy dzia\u0142a z aplikacj\u0105 mobiln\u0105 Flipr.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { @@ -22,7 +22,7 @@ "email": "Adres e-mail", "password": "Has\u0142o" }, - "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta Flipr.", + "description": "Po\u0142\u0105cz, u\u017cywaj\u0105c swojego konta Flipr.", "title": "Po\u0142\u0105czenie z Flipr" } } diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index a111d66b937..5fe7fcf2ca4 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9." + "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", diff --git a/homeassistant/components/forecast_solar/translations/fr.json b/homeassistant/components/forecast_solar/translations/fr.json index a6091b9c21b..efd9f7be3a6 100644 --- a/homeassistant/components/forecast_solar/translations/fr.json +++ b/homeassistant/components/forecast_solar/translations/fr.json @@ -3,8 +3,28 @@ "step": { "user": { "data": { + "azimuth": "Azimut (360 degr\u00e9s, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ouest)", + "declination": "D\u00e9clinaison (0 = horizontale, 90 = verticale)", + "latitude": "Latitude", + "longitude": "Longitude", + "modules power": "Puissance de cr\u00eate totale en watts de vos modules solaires", "name": "Nom" - } + }, + "description": "Remplissez les donn\u00e9es de vos panneaux solaires. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation si un champ n'est pas clair." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Cl\u00e9 API Forecast.Solar (facultatif)", + "azimuth": "Azimut (360 degr\u00e9s, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ouest)", + "damping": "Facteur d'amortissement : ajuste les r\u00e9sultats matin et soir", + "declination": "D\u00e9clinaison (0 = horizontale, 90 = verticale)", + "modules power": "Puissance de cr\u00eate totale en watts de vos modules solaires" + }, + "description": "Ces valeurs permettent de peaufiner le r\u00e9sultat Solar.Forecast. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation si un champ n'est pas clair." } } } diff --git a/homeassistant/components/freedompro/translations/fr.json b/homeassistant/components/freedompro/translations/fr.json index 4822faaef86..6667226a206 100644 --- a/homeassistant/components/freedompro/translations/fr.json +++ b/homeassistant/components/freedompro/translations/fr.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, "step": { "user": { + "data": { + "api_key": "cl\u00e9 API" + }, + "description": "Veuillez saisir la cl\u00e9 API obtenue sur https://home.freedompro.eu", "title": "Cl\u00e9 API Freedompro" } } diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index e0fa5dd3e8c..6518b5ed20c 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration est d\u00e9j\u00e0 en cours", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9 ", + "already_in_progress": "Le flux de configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", "connection_error": "Erreur de connexion", "invalid_auth": "Authentification invalide" }, @@ -35,6 +39,25 @@ }, "description": "Configuration de FRITZ!Box Tools pour contr\u00f4ler votre FRITZ!Box.\nMinimum requis: nom d'utilisateur, mot de passe.", "title": "Configuration FRITZ!Box Tools - obligatoire" + }, + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "description": "Configurer FRITZ!Box Tools pour contr\u00f4ler votre FRITZ!Box.\n Minimum requis : nom d'utilisateur, mot de passe.", + "title": "Configurer les outils de FRITZ!Box" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'" + } } } } diff --git a/homeassistant/components/garages_amsterdam/translations/fr.json b/homeassistant/components/garages_amsterdam/translations/fr.json new file mode 100644 index 00000000000..68530899d1e --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "garage_name": "Nom du garage" + }, + "title": "Choisisser un garage \u00e0 surveiller" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 7bd4929ad92..bb6a777b6be 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +11,10 @@ "unknown": "Erreur inconnue" }, "step": { + "confirm_discovery": { + "description": "La r\u00e9servation DHCP sur votre routeur est recommand\u00e9e. S'il n'est pas configur\u00e9, l'appareil peut devenir indisponible jusqu'\u00e0 ce que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur.", + "title": "Objectif Z\u00e9ro Y\u00e9ti" + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json index 79f216738c4..94cee628a79 100644 --- a/homeassistant/components/gogogate2/translations/fr.json +++ b/homeassistant/components/gogogate2/translations/fr.json @@ -7,6 +7,7 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index f0aa3b0c2d8..0ca969e6812 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -1,13 +1,13 @@ { "state": { "_": { - "closed": "\u05e0\u05e1\u05d2\u05e8", + "closed": "\u05e1\u05d2\u05d5\u05e8", "home": "\u05d1\u05d1\u05d9\u05ea", "locked": "\u05e0\u05e2\u05d5\u05dc", "not_home": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", "off": "\u05db\u05d1\u05d5\u05d9", "ok": "\u05ea\u05e7\u05d9\u05df", - "on": "\u05d3\u05dc\u05d5\u05e7", + "on": "\u05de\u05d5\u05e4\u05e2\u05dc", "open": "\u05e4\u05ea\u05d5\u05d7", "problem": "\u05d1\u05e2\u05d9\u05d4", "unlocked": "\u05e4\u05ea\u05d5\u05d7" diff --git a/homeassistant/components/growatt_server/translations/cs.json b/homeassistant/components/growatt_server/translations/cs.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/fr.json b/homeassistant/components/growatt_server/translations/fr.json new file mode 100644 index 00000000000..1ad47166f8d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "no_plants": "Aucune plante n'a \u00e9t\u00e9 trouv\u00e9e sur ce compte" + }, + "error": { + "invalid_auth": "Authentification incorrecte" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plante" + }, + "title": "S\u00e9lectionner votre plante" + }, + "user": { + "data": { + "name": "Nom", + "password": "Mot de passe", + "url": "URL", + "username": "Nom d'utilisateur" + }, + "title": "Entrer vos informations Growatt" + } + } + }, + "title": "Serveur Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/he.json b/homeassistant/components/growatt_server/translations/he.json index 14b47532075..8d430b5f4b2 100644 --- a/homeassistant/components/growatt_server/translations/he.json +++ b/homeassistant/components/growatt_server/translations/he.json @@ -4,6 +4,11 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "plant": { + "data": { + "plant_id": "\u05e6\u05de\u05d7" + } + }, "user": { "data": { "name": "\u05e9\u05dd", diff --git a/homeassistant/components/growatt_server/translations/it.json b/homeassistant/components/growatt_server/translations/it.json index 19862f82d83..a3160c4164b 100644 --- a/homeassistant/components/growatt_server/translations/it.json +++ b/homeassistant/components/growatt_server/translations/it.json @@ -17,6 +17,7 @@ "data": { "name": "Nome", "password": "Password", + "url": "URL", "username": "Utente" }, "title": "Inserisci le tue informazioni Growatt" diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index ca5635a17b7..62ffae35776 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -6,6 +6,9 @@ "cannot_connect": "\u00c9chec de connexion" }, "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer cet appareil Guardian\u00a0?" + }, "user": { "data": { "ip_address": "Adresse IP", diff --git a/homeassistant/components/harmony/translations/he.json b/homeassistant/components/harmony/translations/he.json index 1331c17e961..49470b50ca9 100644 --- a/homeassistant/components/harmony/translations/he.json +++ b/homeassistant/components/harmony/translations/he.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/home_plus_control/translations/de.json b/homeassistant/components/home_plus_control/translations/de.json index 8e7d9e9bc24..8cb47ae3fec 100644 --- a/homeassistant/components/home_plus_control/translations/de.json +++ b/homeassistant/components/home_plus_control/translations/de.json @@ -17,5 +17,5 @@ } } }, - "title": "" + "title": "Legrand Home+ Steuerung" } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 0fb983f1a20..c9afa13fb85 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domini da includere" }, - "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale TV e telecamera.", + "description": "Scegli i domini da includere. Tutte le entit\u00e0 supportate nel dominio saranno incluse. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e telecamera.", "title": "Seleziona i domini da includere" } } diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 5ae7a0faafd..faf970f88eb 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Code HomeKit incorrect. S'il vous pla\u00eet v\u00e9rifier et essayez \u00e0 nouveau.", + "insecure_setup_code": "Le code de configuration demand\u00e9 n'est pas s\u00e9curis\u00e9 en raison de sa nature triviale. Cet accessoire ne r\u00e9pond pas aux exigences de s\u00e9curit\u00e9 de base.", "max_peers_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il ne dispose pas de stockage de couplage libre.", "pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.", "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Autoriser le jumelage avec des codes de configuration non s\u00e9curis\u00e9s.", "pairing_code": "Code d\u2019appairage" }, "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", diff --git a/homeassistant/components/honeywell/translations/cs.json b/homeassistant/components/honeywell/translations/cs.json new file mode 100644 index 00000000000..25ad431df4e --- /dev/null +++ b/homeassistant/components/honeywell/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/fr.json b/homeassistant/components/honeywell/translations/fr.json index 506e14ab26f..b9b625eb589 100644 --- a/homeassistant/components/honeywell/translations/fr.json +++ b/homeassistant/components/honeywell/translations/fr.json @@ -1,10 +1,16 @@ { "config": { + "error": { + "invalid_auth": "Authentification incorrecte" + }, "step": { "user": { "data": { - "password": "Mot de passe" - } + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez saisir les informations d'identification utilis\u00e9es pour vous connecter \u00e0 mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (\u00c9tats-Unis)" } } } diff --git a/homeassistant/components/honeywell/translations/it.json b/homeassistant/components/honeywell/translations/it.json new file mode 100644 index 00000000000..52c828ddcde --- /dev/null +++ b/homeassistant/components/honeywell/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci le credenziali utilizzate per accedere a mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index df7e6c2e380..da8fbcbd115 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -35,7 +35,8 @@ "name": "Nom du service de notification (red\u00e9marrage requis)", "recipient": "Destinataires des notifications SMS", "track_new_devices": "Suivre les nouveaux appareils", - "track_wired_clients": "Suivre les clients du r\u00e9seau filaire" + "track_wired_clients": "Suivre les clients du r\u00e9seau filaire", + "unauthenticated_mode": "Mode non authentifi\u00e9 (le changement n\u00e9cessite un rechargement)" } } } diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index f19c5ec7a34..e9dd546840f 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -11,13 +11,13 @@ "unknown": "Une erreur inconnue s'est produite" }, "error": { - "linking": "Une erreur inconnue s'est produite lors de la liaison entre le pont et Home Assistant", + "linking": "Erreur inattendue", "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." }, "step": { "init": { "data": { - "host": "Nom d'h\u00f4te ou adresse IP" + "host": "H\u00f4te" }, "title": "Choisissez le pont Philips Hue" }, diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json index a1bd06078c6..68ea30b293f 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/fr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json @@ -7,6 +7,7 @@ "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "unknown": "Erreur inattendue" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Voulez-vous configurer {name} ({host})?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/he.json b/homeassistant/components/hunterdouglas_powerview/translations/he.json index c6610f79e77..8bd6c87154c 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/he.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/he.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/input_boolean/translations/he.json b/homeassistant/components/input_boolean/translations/he.json index 08bdc30a602..b5d50c10627 100644 --- a/homeassistant/components/input_boolean/translations/he.json +++ b/homeassistant/components/input_boolean/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05e7\u05dc\u05d8 \u05d1\u05d5\u05dc\u05d9\u05d0\u05e0\u05d9" diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index 01f2145cd12..0bd04cd14b1 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -36,5 +36,13 @@ "title": "Options ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY connect\u00e9", + "host_reachable": "H\u00f4te joignable", + "last_heartbeat": "Heure du dernier pulsation", + "websocket_status": "\u00c9tat du socket d'\u00e9v\u00e9nement" + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/fr.json b/homeassistant/components/keenetic_ndms2/translations/fr.json index bf3ebbf5b22..74685da509b 100644 --- a/homeassistant/components/keenetic_ndms2/translations/fr.json +++ b/homeassistant/components/keenetic_ndms2/translations/fr.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "no_udn": "Les informations de d\u00e9couverte SSDP n'ont pas d'UDN", + "not_keenetic_ndms2": "L'\u00e9l\u00e9ment d\u00e9couvert n'est pas un routeur Keenetic" }, "error": { "cannot_connect": "\u00c9chec de connexion" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json index 04e02ee88a0..5b992705068 100644 --- a/homeassistant/components/kodi/translations/he.json +++ b/homeassistant/components/kodi/translations/he.json @@ -22,7 +22,7 @@ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05d1-Kodi. \u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05dd \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd." }, "discovery_confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9 ((`{name}`) \u05dc-Home Assistant?", + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9 (`{name}`) \u05dc-Home Assistant?", "title": "\u05d2\u05d9\u05dc\u05d4 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9" }, "user": { diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index 9e791916df7..0a436bc2d3c 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -22,14 +22,30 @@ } }, "options": { + "error": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "step": { "options_binary": { "data": { "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" } }, + "options_digital": { + "data": { + "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } + }, "options_io": { "description": "\u05d4\u05ea\u05d2\u05dc\u05d4 {model} \u05d1-{host} . \u05d1\u05d7\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d1\u05e1\u05d9\u05e1 \u05e9\u05dc \u05db\u05dc \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05dc\u05de\u05d8\u05d4 - \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d6\u05d4 \u05e2\u05e9\u05d5\u05d9 \u05dc\u05d0\u05e4\u05e9\u05e8 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd (\u05de\u05d2\u05e2\u05d9\u05dd \u05e4\u05ea\u05d5\u05d7\u05d9\u05dd/\u05e1\u05d2\u05d5\u05e8\u05d9\u05dd), \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d3\u05d9\u05d2\u05d9\u05d8\u05dc\u05d9\u05d9\u05dd (dht \u05d5-ds18b20), \u05d0\u05d5 \u05d9\u05e6\u05d9\u05d0\u05d5\u05ea \u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d7\u05dc\u05e4\u05d4. \u05ea\u05d5\u05db\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05e4\u05d5\u05e8\u05d8\u05d5\u05ea \u05d1\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05d1\u05d0\u05d9\u05dd." + }, + "options_switch": { + "data": { + "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } } } } diff --git a/homeassistant/components/kraken/translations/fr.json b/homeassistant/components/kraken/translations/fr.json new file mode 100644 index 00000000000..1aa7fdfbf54 --- /dev/null +++ b/homeassistant/components/kraken/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "one": "UN", + "other": "AUTRE" + }, + "step": { + "user": { + "data": { + "one": "UN", + "other": "AUTRE" + }, + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle de mise \u00e0 jour", + "tracked_asset_pairs": "Paires d'actifs suivis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/he.json b/homeassistant/components/kraken/translations/he.json index 4676729e600..2be0837c966 100644 --- a/homeassistant/components/kraken/translations/he.json +++ b/homeassistant/components/kraken/translations/he.json @@ -3,10 +3,31 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "error": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "step": { "user": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e2\u05d3\u05db\u05d5\u05df" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json index 89459d1829f..b8ca0adb3d4 100644 --- a/homeassistant/components/litejet/translations/fr.json +++ b/homeassistant/components/litejet/translations/fr.json @@ -15,5 +15,15 @@ "title": "Connectez-vous \u00e0 LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transition par d\u00e9faut (secondes)" + }, + "title": "Configurer LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/it.json b/homeassistant/components/litejet/translations/it.json index 5b3dc46753d..0b34e6fb365 100644 --- a/homeassistant/components/litejet/translations/it.json +++ b/homeassistant/components/litejet/translations/it.json @@ -15,5 +15,15 @@ "title": "Connetti a LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transizione predefinita (secondi)" + }, + "title": "Configura LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json index db23120b40d..9eb02fc3811 100644 --- a/homeassistant/components/lyric/translations/fr.json +++ b/homeassistant/components/lyric/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "create_entry": { "default": "Authentification r\u00e9ussie" @@ -12,7 +13,8 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { - "description": "L'int\u00e9gration Lyric doit authentifier \u00e0 nouveau votre compte." + "description": "L'int\u00e9gration Lyric doit authentifier \u00e0 nouveau votre compte.", + "title": "R\u00e9authentification de l'int\u00e9gration" } } } diff --git a/homeassistant/components/meteoclimatic/translations/fr.json b/homeassistant/components/meteoclimatic/translations/fr.json new file mode 100644 index 00000000000..ae087db2e6e --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "unknown": "Erreur inattendue" + }, + "error": { + "not_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "step": { + "user": { + "data": { + "code": "Code de la station" + }, + "description": "Entrer le code de la station m\u00e9t\u00e9orologique (par exemple, ESCAT4300000043206)", + "title": "M\u00e9t\u00e9oclimatique" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json index 1c11717c1b2..1a9c3b5d352 100644 --- a/homeassistant/components/mikrotik/translations/de.json +++ b/homeassistant/components/mikrotik/translations/de.json @@ -16,9 +16,9 @@ "password": "Passwort", "port": "Port", "username": "Benutzername", - "verify_ssl": "Verwenden Sie SSL" + "verify_ssl": "SSL verwenden" }, - "title": "Richten Sie den Mikrotik Router ein" + "title": "Mikrotik Router einrichten" } } }, @@ -28,7 +28,7 @@ "data": { "arp_ping": "ARP-Ping aktivieren", "detection_time": "Heimintervall ber\u00fccksichtigen", - "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" + "force_dhcp": "Scannen mit DHCP erzwingen" } } } diff --git a/homeassistant/components/modern_forms/translations/fr.json b/homeassistant/components/modern_forms/translations/fr.json new file mode 100644 index 00000000000..d68f4a7f680 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurer votre ventilateur Modern Forms pour l'int\u00e9grer \u00e0 Home Assistant." + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le fan de Modern Forms nomm\u00e9 ` {name} ` \u00e0 Home Assistant\u00a0?", + "title": "D\u00e9couverte du dispositif de ventilateur Modern Forms" + } + } + }, + "title": "Formes modernes" +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index 338bc392bd5..b8d79b683a6 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -1,18 +1,27 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { - "invalid_url": "URL invalide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "invalid_url": "URL invalide", + "unknown": "Erreur inattendue" }, "step": { + "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour vous connecter au service motionEye fourni par le module compl\u00e9mentaire\u00a0: {addon}\u00a0?", + "title": "motionEye via le module compl\u00e9mentaire Home Assistant" + }, "user": { "data": { "admin_password": "Admin Mot de passe", "admin_username": "Admin Nom d'utilisateur", "surveillance_password": "Surveillance Mot de passe", - "surveillance_username": "Surveillance Nom d'utilisateur" + "surveillance_username": "Surveillance Nom d'utilisateur", + "url": "URL" } } } diff --git a/homeassistant/components/myq/translations/fr.json b/homeassistant/components/myq/translations/fr.json index e9a6bc60b82..c07e3710645 100644 --- a/homeassistant/components/myq/translations/fr.json +++ b/homeassistant/components/myq/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "MyQ est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "MyQ est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index bb6a1ed7bfe..cfef6f7d363 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -76,5 +76,5 @@ } } }, - "title": "" + "title": "MySensors" } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json index 0c58af2a800..1800e6da508 100644 --- a/homeassistant/components/nam/translations/fr.json +++ b/homeassistant/components/nam/translations/fr.json @@ -4,6 +4,10 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "device_unsupported": "L'appareil n'est pas pris en charge." }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, "flow_title": "{nom}", "step": { "confirm_discovery": { diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index dab438c24a8..a3b5411b536 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", diff --git a/homeassistant/components/nexia/translations/fr.json b/homeassistant/components/nexia/translations/fr.json index 8082f912bed..5cec9b66836 100644 --- a/homeassistant/components/nexia/translations/fr.json +++ b/homeassistant/components/nexia/translations/fr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marque", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/nfandroidtv/translations/cs.json b/homeassistant/components/nfandroidtv/translations/cs.json new file mode 100644 index 00000000000..b268d8945a0 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/fr.json b/homeassistant/components/nfandroidtv/translations/fr.json new file mode 100644 index 00000000000..6d00852889b --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom" + }, + "description": "Cette int\u00e9gration n\u00e9cessite l'application Notifications pour Android TV. \n\nPour Android TV\u00a0: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPour Fire TV\u00a0: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nVous devez configurer soit une r\u00e9servation DHCP sur votre routeur (reportez-vous au manuel d'utilisation de votre routeur) soit une adresse IP statique sur l'appareil. Sinon, l'appareil finira par devenir indisponible.", + "title": "Notifications pour Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/it.json b/homeassistant/components/nfandroidtv/translations/it.json new file mode 100644 index 00000000000..3b8d089b5a5 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome" + }, + "description": "Questa integrazione richiede l'app Notifiche per Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n\u00c8 necessario impostare la prenotazione DHCP sul router (fare riferimento al manuale utente del router) o un indirizzo IP statico sul dispositivo. In caso contrario, il dispositivo alla fine non sar\u00e0 pi\u00f9 disponibile.", + "title": "Notifiche per Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/nl.json b/homeassistant/components/nfandroidtv/translations/nl.json index b231bd00c3c..acd936abe70 100644 --- a/homeassistant/components/nfandroidtv/translations/nl.json +++ b/homeassistant/components/nfandroidtv/translations/nl.json @@ -13,6 +13,7 @@ "host": "Host", "name": "Naam" }, + "description": "Voor deze integratie is de app Notifications for Android TV vereist.\n\nVoor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nVoor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nU moet een DHCP-reservering op uw router instellen (raadpleeg de gebruikershandleiding van uw router) of een statisch IP-adres op het apparaat instellen. Zo niet, dan zal het apparaat uiteindelijk onbeschikbaar worden.", "title": "Meldingen voor Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/pl.json b/homeassistant/components/nfandroidtv/translations/pl.json index 597d8bb9200..4dd742b7c1f 100644 --- a/homeassistant/components/nfandroidtv/translations/pl.json +++ b/homeassistant/components/nfandroidtv/translations/pl.json @@ -13,7 +13,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Ta integracja wymaga aplikacji Powiadomienia dla Androida TV. \n\nAndroid TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nNale\u017cy skonfigurowa\u0107 rezerwacj\u0119 DHCP na routerze (patrz instrukcja obs\u0142ugi routera) lub statyczny adres IP na urz\u0105dzeniu. Je\u015bli tego nie zrobisz, urz\u0105dzenie ostatecznie stanie si\u0119 niedost\u0119pne.", + "description": "Ta integracja wymaga aplikacji Powiadomienia dla Androida TV. \n\nAndroid TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nNale\u017cy skonfigurowa\u0107 rezerwacj\u0119 DHCP na routerze (patrz instrukcja obs\u0142ugi routera) lub ustawi\u0107 statyczny adres IP na urz\u0105dzeniu. Je\u015bli tego nie zrobisz, urz\u0105dzenie ostatecznie stanie si\u0119 niedost\u0119pne.", "title": "Powiadomienia dla Android TV / Fire TV" } } diff --git a/homeassistant/components/nmap_tracker/translations/cs.json b/homeassistant/components/nmap_tracker/translations/cs.json new file mode 100644 index 00000000000..1a0d0ae0b53 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/fr.json b/homeassistant/components/nmap_tracker/translations/fr.json index f4302a4173c..69d7d58f2e6 100644 --- a/homeassistant/components/nmap_tracker/translations/fr.json +++ b/homeassistant/components/nmap_tracker/translations/fr.json @@ -1,12 +1,40 @@ { + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_hosts": "H\u00f4tes invalides" + }, + "step": { + "user": { + "data": { + "exclude": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 exclure de l'analyse", + "home_interval": "Nombre minimum de minutes entre les analyses des appareils actifs (pr\u00e9server la batterie)", + "hosts": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 analyser", + "scan_options": "Options d'analyse brutes configurables pour Nmap" + }, + "description": "Configurer les h\u00f4tes \u00e0 analyser par Nmap. L'adresse r\u00e9seau et les exclusions peuvent \u00eatre des adresses IP (192.168.1.1), des r\u00e9seaux IP (192.168.0.0/24) ou des plages IP (192.168.1.0-32)." + } + } + }, "options": { + "error": { + "invalid_hosts": "H\u00f4tes invalides" + }, "step": { "init": { "data": { + "exclude": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 exclure de l'analyse", + "home_interval": "Nombre minimum de minutes entre les analyses des appareils actifs (pr\u00e9server la batterie)", + "hosts": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 analyser", "interval_seconds": "Intervalle d\u2019analyse", + "scan_options": "Options d'analyse brutes configurables pour Nmap", "track_new_devices": "Suivre les nouveaux appareils" - } + }, + "description": "Configurer les h\u00f4tes \u00e0 analyser par Nmap. L'adresse r\u00e9seau et les exclusions peuvent \u00eatre des adresses IP (192.168.1.1),R\u00e9seaux IP (192.168.0.0/24) ou plages IP (192.168.1.0-32)." } } - } + }, + "title": "Traqueur Nmap" } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index d8d1cf89611..76eb733db3d 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -18,6 +18,16 @@ }, "title": "Configurer l'authentification" }, + "configure": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configurer le p\u00e9riph\u00e9rique ONVIF" + }, "configure_profile": { "data": { "include": "Cr\u00e9er une entit\u00e9 cam\u00e9ra" @@ -40,6 +50,9 @@ "title": "Configurer l\u2019appareil ONVIF" }, "user": { + "data": { + "auto": "Rechercher automatiquement" + }, "description": "En cliquant sur soumettre, nous rechercherons sur votre r\u00e9seau, des \u00e9quipements ONVIF qui supporte le Profile S.\n\nCertains constructeurs ont commenc\u00e9 \u00e0 d\u00e9sactiver ONvif par d\u00e9faut. Assurez-vous qu\u2019ONVIF est activ\u00e9 dans la configuration de votre cam\u00e9ra", "title": "Configuration de l'appareil ONVIF" } diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index 290effbc48e..6d1229f1a5d 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05dc\u05da.", + "no_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d9\u05e9 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da.", "no_mac": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF." }, "error": { diff --git a/homeassistant/components/ovo_energy/translations/he.json b/homeassistant/components/ovo_energy/translations/he.json index 7864218bc3b..270d8744b96 100644 --- a/homeassistant/components/ovo_energy/translations/he.json +++ b/homeassistant/components/ovo_energy/translations/he.json @@ -17,7 +17,8 @@ "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d5\u05e4\u05e2 OVO \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d2\u05e9\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc\u05da." + "description": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d5\u05e4\u05e2 OVO \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d2\u05e9\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc\u05da.", + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05d7\u05e9\u05d1\u05d5\u05df \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc OVO" } } } diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json index eb16bb92271..8cc5d187743 100644 --- a/homeassistant/components/philips_js/translations/fr.json +++ b/homeassistant/components/philips_js/translations/fr.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 l'appareil de s'allumer" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Autoriser l'utilisation du service de notification de donn\u00e9es." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json index 044b0a72771..75e35a951de 100644 --- a/homeassistant/components/picnic/translations/fr.json +++ b/homeassistant/components/picnic/translations/fr.json @@ -3,6 +3,11 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index a89120b85ab..db3eeef2d53 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -10,6 +10,9 @@ }, "flow_title": "{name}", "step": { + "user": { + "description": "\u05de\u05d5\u05e6\u05e8:" + }, "user_gateway": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 0f82f949ede..5ce9313b442 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -13,7 +13,7 @@ "password": "Passwort" }, "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", - "title": "" + "title": "PoolSense" } } } diff --git a/homeassistant/components/prosegur/translations/ca.json b/homeassistant/components/prosegur/translations/ca.json new file mode 100644 index 00000000000..a6c7c925a25 --- /dev/null +++ b/homeassistant/components/prosegur/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Torna a autenticar-te amb el compte de Prosegur.", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, + "user": { + "data": { + "country": "Pa\u00eds", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/cs.json b/homeassistant/components/prosegur/translations/cs.json new file mode 100644 index 00000000000..13c0827ff40 --- /dev/null +++ b/homeassistant/components/prosegur/translations/cs.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/de.json b/homeassistant/components/prosegur/translations/de.json new file mode 100644 index 00000000000..aa5667d8d54 --- /dev/null +++ b/homeassistant/components/prosegur/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Authentifiziere dich erneut mit deinem Prosegur-Konto.", + "password": "Passwort", + "username": "Benutzername" + } + }, + "user": { + "data": { + "country": "Land", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/et.json b/homeassistant/components/prosegur/translations/et.json new file mode 100644 index 00000000000..bc5a1a5a7ea --- /dev/null +++ b/homeassistant/components/prosegur/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu viga" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Taastuvasta oma Prosegur kontoga.", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, + "user": { + "data": { + "country": "Riik", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/fr.json b/homeassistant/components/prosegur/translations/fr.json new file mode 100644 index 00000000000..7c0d361da6a --- /dev/null +++ b/homeassistant/components/prosegur/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "R\u00e9-authentifiez-vous avec le compte Prosegur.", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, + "user": { + "data": { + "country": "Pays", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/he.json b/homeassistant/components/prosegur/translations/he.json new file mode 100644 index 00000000000..89a7445f2c3 --- /dev/null +++ b/homeassistant/components/prosegur/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "country": "\u05de\u05d3\u05d9\u05e0\u05d4", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/it.json b/homeassistant/components/prosegur/translations/it.json new file mode 100644 index 00000000000..79409045d7b --- /dev/null +++ b/homeassistant/components/prosegur/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Eseguire nuovamente l'autenticazione con l'account Prosegur.", + "password": "Password", + "username": "Nome utente" + } + }, + "user": { + "data": { + "country": "Nazione", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/nl.json b/homeassistant/components/prosegur/translations/nl.json new file mode 100644 index 00000000000..d87556b0742 --- /dev/null +++ b/homeassistant/components/prosegur/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Verifieer opnieuw met Prosegur-account.", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, + "user": { + "data": { + "country": "Land", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/no.json b/homeassistant/components/prosegur/translations/no.json new file mode 100644 index 00000000000..5732bb920b2 --- /dev/null +++ b/homeassistant/components/prosegur/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/pl.json b/homeassistant/components/prosegur/translations/pl.json new file mode 100644 index 00000000000..342c17222b2 --- /dev/null +++ b/homeassistant/components/prosegur/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Prosegur.", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "country": "Kraj", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/ru.json b/homeassistant/components/prosegur/translations/ru.json new file mode 100644 index 00000000000..c75f3e572c6 --- /dev/null +++ b/homeassistant/components/prosegur/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth_confirm": { + "data": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Prosegur.", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "user": { + "data": { + "country": "\u0421\u0442\u0440\u0430\u043d\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/zh-Hant.json b/homeassistant/components/prosegur/translations/zh-Hant.json new file mode 100644 index 00000000000..501e979448e --- /dev/null +++ b/homeassistant/components/prosegur/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "\u91cd\u65b0\u8a8d\u8b49 Prosegur \u5e33\u865f\u3002", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "user": { + "data": { + "country": "\u570b\u5bb6", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index 837dd13b925..e9543da8206 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 \u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4 \u05d1\u05e8\u05e9\u05ea." + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" @@ -14,7 +14,7 @@ "link": { "data": { "code": "\u05e7\u05d5\u05d3 PIN", - "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4 - IP", + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "name": "\u05e9\u05dd", "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" }, diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 626382fcb6f..545a3d2cd9f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -11,7 +11,7 @@ "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Geltender Tarif nach geografischer Zone" }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen findest du in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). ", + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen findest du in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "title": "Sensoreinrichtung" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index 5386529e43a..f8511a80579 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -7,11 +7,26 @@ "user": { "data": { "name": "Nom du capteur", + "power": "Puissance souscrite (kW)", + "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", "tariff": "Tarif souscrit (1, 2, ou 3 p\u00e9riodes)" }, "description": "Ce capteur utilise l'API officielle pour obtenir la [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espagne. \n Pour une explication plus pr\u00e9cise, visitez la [documentation d'int\u00e9gration] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n S\u00e9lectionnez le tarif contract\u00e9 en fonction du nombre de p\u00e9riodes de facturation par jour: \n - 1 p\u00e9riode: normale \n - 2 p\u00e9riodes: discrimination (tarif \u00e0 la nuit) \n - 3 p\u00e9riodes: voiture \u00e9lectrique (tarif \u00e0 la nuit sur 3 p\u00e9riodes)", "title": "S\u00e9lection tarifaire" } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Puissance souscrite (kW)", + "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", + "tariff": "Tarif applicable par zone g\u00e9ographique" + }, + "description": "Ce capteur utilise l'API officielle pour obtenir [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)](https://www.esios.ree.es/es/pvpc) en Espagne.\n Pour des explications plus pr\u00e9cises, visitez les [docs d'int\u00e9gration](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configuration du capteur" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/he.json b/homeassistant/components/pvpc_hourly_pricing/translations/he.json index 48a6eeeea33..951e9b21b2f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/he.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/he.json @@ -3,5 +3,12 @@ "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" } + }, + "options": { + "step": { + "init": { + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d7\u05d9\u05d9\u05e9\u05df" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ca.json b/homeassistant/components/renault/translations/ca.json new file mode 100644 index 00000000000..8315d35b87b --- /dev/null +++ b/homeassistant/components/renault/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "kamereon_no_account": "No s'ha pogut trobar cap compte Kamereon." + }, + "error": { + "invalid_credentials": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID del compte Kamereon" + }, + "title": "Seleccioneu l'ID del compte Kamereon" + }, + "user": { + "data": { + "locale": "Llengua/regi\u00f3", + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Defineix les credencials de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/cs.json b/homeassistant/components/renault/translations/cs.json new file mode 100644 index 00000000000..d731b4c2ec0 --- /dev/null +++ b/homeassistant/components/renault/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "invalid_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/de.json b/homeassistant/components/renault/translations/de.json new file mode 100644 index 00000000000..16650b8d63e --- /dev/null +++ b/homeassistant/components/renault/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "kamereon_no_account": "Kamereon-Konto kann nicht gefunden werden." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon-Kontonummer" + }, + "title": "Kamereon-Kontonummer ausw\u00e4hlen" + }, + "user": { + "data": { + "locale": "Gebietsschema", + "password": "Passwort", + "username": "E-Mail" + }, + "title": "Renault-Anmeldeinformationen festlegen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/en.json b/homeassistant/components/renault/translations/en.json index bb65493a3b3..87186e6f59c 100644 --- a/homeassistant/components/renault/translations/en.json +++ b/homeassistant/components/renault/translations/en.json @@ -1,27 +1,27 @@ { - "config": { - "abort": { - "already_configured": "Account already configured", - "kamereon_no_account": "Unable to find Kamereon account." - }, - "error": { - "invalid_credentials": "Invalid credentials." - }, - "step": { - "kamereon": { - "data": { - "kamereon_account_id": "Kamereon account id" + "config": { + "abort": { + "already_configured": "Account is already configured", + "kamereon_no_account": "Unable to find Kamereon account." }, - "title": "Select Kamereon account id" - }, - "user": { - "data": { - "locale": "Locale", - "username": "Email", - "password": "Password" + "error": { + "invalid_credentials": "Invalid authentication" }, - "title": "Set Renault credentials" - } + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Select Kamereon account id" + }, + "user": { + "data": { + "locale": "Locale", + "password": "Password", + "username": "Email" + }, + "title": "Set Renault credentials" + } + } } - } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/et.json b/homeassistant/components/renault/translations/et.json new file mode 100644 index 00000000000..bae0db1aed7 --- /dev/null +++ b/homeassistant/components/renault/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "kamereon_no_account": "Kamereoni kontot ei leitud." + }, + "error": { + "invalid_credentials": "Tuvastamine nurjus" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereoni konto ID" + }, + "title": "Vali Kamereoni konto ID" + }, + "user": { + "data": { + "locale": "Riigi kood (n\u00e4iteks EE)", + "password": "Salas\u00f5na", + "username": "E-posti aadress" + }, + "title": "M\u00e4\u00e4ra Renault sidumise parameetrid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json new file mode 100644 index 00000000000..874a9b8df67 --- /dev/null +++ b/homeassistant/components/renault/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "kamereon_no_account": "Impossible de trouver le compte Kamereon." + }, + "error": { + "invalid_credentials": "Authentification incorrecte" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Identifiant du compte Kamereon" + }, + "title": "S\u00e9lectionner l'identifiant du compte Kamereon" + }, + "user": { + "data": { + "locale": "Lieu", + "password": "Mot de passe", + "username": "Email" + }, + "title": "D\u00e9finir les informations d'identification de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/he.json b/homeassistant/components/renault/translations/he.json new file mode 100644 index 00000000000..d20e2d36a81 --- /dev/null +++ b/homeassistant/components/renault/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_credentials": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/it.json b/homeassistant/components/renault/translations/it.json new file mode 100644 index 00000000000..37ba94b3cdf --- /dev/null +++ b/homeassistant/components/renault/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "kamereon_no_account": "Impossibile trovare l'account Kamereon." + }, + "error": { + "invalid_credentials": "Autenticazione non valida" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID account Kamereon" + }, + "title": "Seleziona l'id dell'account Kamereon" + }, + "user": { + "data": { + "locale": "Locale", + "password": "Password", + "username": "E-mail" + }, + "title": "Imposta le credenziali Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json new file mode 100644 index 00000000000..4840dd0c07b --- /dev/null +++ b/homeassistant/components/renault/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "kamereon_no_account": "Kan Kamereon-account niet vinden." + }, + "error": { + "invalid_credentials": "Ongeldige authenticatie" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Selecteer Kamereon-account-ID" + }, + "user": { + "data": { + "locale": "Locale", + "password": "Wachtwoord", + "username": "E-mail" + }, + "title": "Renault-inloggegevens instellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/no.json b/homeassistant/components/renault/translations/no.json new file mode 100644 index 00000000000..f367c8c540d --- /dev/null +++ b/homeassistant/components/renault/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-Post" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/pl.json b/homeassistant/components/renault/translations/pl.json new file mode 100644 index 00000000000..1d518cc14fb --- /dev/null +++ b/homeassistant/components/renault/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "kamereon_no_account": "Nie mo\u017cna znale\u017a\u0107 konta Kamereon." + }, + "error": { + "invalid_credentials": "Niepoprawne uwierzytelnienie" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Identyfikator konta Kamereon" + }, + "title": "Wyb\u00f3r identyfikatora konta Kamereon" + }, + "user": { + "data": { + "locale": "Ustawienia regionalne", + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Dane logowania Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ru.json b/homeassistant/components/renault/translations/ru.json new file mode 100644 index 00000000000..822d42b6117 --- /dev/null +++ b/homeassistant/components/renault/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "kamereon_no_account": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Kamereon." + }, + "error": { + "invalid_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Kamereon" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 ID \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Kamereon" + }, + "user": { + "data": { + "locale": "\u0420\u0435\u0433\u0438\u043e\u043d", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/zh-Hant.json b/homeassistant/components/renault/translations/zh-Hant.json new file mode 100644 index 00000000000..4ae5413499d --- /dev/null +++ b/homeassistant/components/renault/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "kamereon_no_account": "\u627e\u4e0d\u5230 Kamereon \u5e33\u865f\u3002" + }, + "error": { + "invalid_credentials": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon \u5e33\u865f ID" + }, + "title": "\u9078\u64c7 Kamereon \u5e33\u865f ID" + }, + "user": { + "data": { + "locale": "\u4f4d\u7f6e", + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "title": "\u8a2d\u5b9a Renault \u6191\u8b49" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/he.json b/homeassistant/components/roku/translations/he.json index 41d59c29fd8..12dc4bb482b 100644 --- a/homeassistant/components/roku/translations/he.json +++ b/homeassistant/components/roku/translations/he.json @@ -10,6 +10,14 @@ }, "flow_title": "{name}", "step": { + "ssdp_confirm": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json index e2d0c48b0b9..4520671eedb 100644 --- a/homeassistant/components/roomba/translations/he.json +++ b/homeassistant/components/roomba/translations/he.json @@ -34,7 +34,7 @@ "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05db\u05e8\u05d2\u05e2 \u05d0\u05d7\u05d6\u05d5\u05e8 \u05d4-BLID \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05ea\u05d4\u05dc\u05d9\u05da \u05d9\u05d3\u05e0\u05d9. \u05e0\u05d0 \u05d1\u05e6\u05e2 \u05d0\u05ea \u05d4\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05de\u05ea\u05d5\u05d0\u05e8\u05d9\u05dd \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea Roomba \u05d0\u05d5 Braava." } } } diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index 452436d0e05..df8fa80b4dd 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -16,7 +16,7 @@ "data": { "host": "Host" }, - "description": "Voer de hostnaam of het IP-adres van uw Roon-server in." + "description": "Kon de Roon-server niet vinden, voer de hostnaam of het IP-adres in." } } } diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 15a529c94b2..5a20992d8e5 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -5,7 +5,13 @@ "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. Veuillez v\u00e9rifier les param\u00e8tres de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant.", "cannot_connect": "\u00c9chec de connexion", - "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." + "id_missing": "Cet appareil Samsung n'a pas de num\u00e9ro de s\u00e9rie.", + "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie", + "unknown": "Erreur inattendue" + }, + "error": { + "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. V\u00e9rifiez les param\u00e8tres du Gestionnaire de p\u00e9riph\u00e9riques externes de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant." }, "flow_title": "Samsung TV: {model}", "step": { @@ -13,6 +19,9 @@ "description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", "title": "TV Samsung" }, + "reauth_confirm": { + "description": "Apr\u00e8s avoir soumis, acceptez la fen\u00eatre contextuelle sur {device} demandant l'autorisation dans les 30 secondes." + }, "user": { "data": { "host": "Nom d'h\u00f4te ou adresse IP", diff --git a/homeassistant/components/screenlogic/translations/de.json b/homeassistant/components/screenlogic/translations/de.json index 84f425be218..30e3ee0c726 100644 --- a/homeassistant/components/screenlogic/translations/de.json +++ b/homeassistant/components/screenlogic/translations/de.json @@ -18,7 +18,7 @@ }, "gateway_select": { "data": { - "selected_gateway": "" + "selected_gateway": "Gateway" }, "description": "Die folgenden ScreenLogic-Gateways wurden erkannt. Bitte w\u00e4hle eines aus, um es zu konfigurieren oder w\u00e4hle ein ScreenLogic-Gateway zum manuellen Konfigurieren.", "title": "ScreenLogic" diff --git a/homeassistant/components/select/translations/fr.json b/homeassistant/components/select/translations/fr.json index 7a3633cb309..5248940b2c4 100644 --- a/homeassistant/components/select/translations/fr.json +++ b/homeassistant/components/select/translations/fr.json @@ -1,3 +1,14 @@ { + "device_automation": { + "action_type": { + "select_option": "Modifier l'option {entity_name}" + }, + "condition_type": { + "selected_option": "Option actuellement s\u00e9lectionn\u00e9e {entity_name}" + }, + "trigger_type": { + "current_option_changed": "Modification de l\u2019option {entity_name}" + } + }, "title": "S\u00e9lectionner" } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/fr.json b/homeassistant/components/sia/translations/fr.json new file mode 100644 index 00000000000..2b3188dd082 --- /dev/null +++ b/homeassistant/components/sia/translations/fr.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Le compte n'est pas une valeur hexad\u00e9cimale, veuillez utiliser uniquement 0-9 et AF.", + "invalid_account_length": "Le compte n'est pas de la bonne longueur, il doit faire entre 3 et 16 caract\u00e8res.", + "invalid_key_format": "La cl\u00e9 n'est pas une valeur hexad\u00e9cimale, veuillez utiliser uniquement 0-9 et AF.", + "invalid_key_length": "La cl\u00e9 n'est pas de la bonne longueur, elle doit comporter 16, 24 ou 32 caract\u00e8res hexad\u00e9cimaux.", + "invalid_ping": "L'intervalle de ping doit \u00eatre compris entre 1 et 1440 minutes.", + "invalid_zones": "Il doit y avoir au moins 1 zone.", + "unknown": "Erreur inattendue" + }, + "step": { + "additional_account": { + "data": { + "account": "Identifiant du compte", + "additional_account": "Comptes suppl\u00e9mentaires", + "encryption_key": "Cl\u00e9 de cryptage", + "ping_interval": "Intervalle de ping (min)", + "zones": "Nombre de zones pour le compte" + }, + "title": "Ajouter un autre compte au port actuel." + }, + "user": { + "data": { + "account": "Identifiant de compte", + "additional_account": "Comptes suppl\u00e9mentaires", + "encryption_key": "Cl\u00e9 de cryptage", + "ping_interval": "Intervalle de ping (min)", + "port": "Port", + "protocol": "Protocole", + "zones": "Nombre de zones pour le compte" + }, + "title": "Cr\u00e9er une connexion pour les syst\u00e8mes d'alarme bas\u00e9s sur SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorer la v\u00e9rification de l'horodatage des \u00e9v\u00e9nements SIA", + "zones": "Nombre de zones pour le compte" + }, + "description": "D\u00e9finisser les options du compte\u00a0: {account}", + "title": "Options pour la configuration SIA." + } + } + }, + "title": "Syst\u00e8mes d'alarme SIA" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 966f3598d95..ae354b2138a 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -12,7 +12,7 @@ }, "step": { "mfa": { - "description": "Pr\u00fcfe deine E-Mail auf einen Link von SimpliSafe. Kehre nach der Verifizierung des Links hierher zur\u00fcck, um die Installation der Integration abzuschlie\u00dfen.", + "description": "Pr\u00fcfe deine E-Mail auf einen Link von SimpliSafe. Kehre nach der Verifizierung des Links hierher zur\u00fcck, um die Installation der Integration abzuschlie\u00dfen.", "title": "SimpliSafe Multi-Faktor-Authentifizierung" }, "reauth_confirm": { diff --git a/homeassistant/components/sma/translations/fr.json b/homeassistant/components/sma/translations/fr.json index ab154fea3f8..e70401c87f5 100644 --- a/homeassistant/components/sma/translations/fr.json +++ b/homeassistant/components/sma/translations/fr.json @@ -1,17 +1,23 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration est d\u00e9j\u00e0 en cours" }, "error": { - "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil" + "cannot_connect": "\u00c9chec de connexion", + "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" }, "step": { "user": { "data": { "group": "Groupe", "host": "H\u00f4te ", - "password": "Mot de passe" + "password": "Mot de passe", + "ssl": "Utilise un certificat SSL", + "verify_ssl": "V\u00e9rifier le certificat SSL" }, "description": "Saisissez les informations relatives \u00e0 votre appareil SMA.", "title": "Configurer SMA Solar" diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index c098bfafbee..db5bc91bf7b 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -19,7 +19,7 @@ } }, "user": { - "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", + "description": "SmartThings \u05d9\u05d5\u05d2\u05d3\u05e8 \u05dc\u05e9\u05dc\u05d5\u05d7 \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05d3\u05d7\u05d9\u05e4\u05d4 \u05dc-Home Assistant \u05d1:\n> {webhook_url}\n\n\u05d0\u05dd \u05d4\u05d3\u05d1\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05db\u05d5\u05df, \u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", "title": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d4\u05ea\u05e7\u05e9\u05e8\u05d5\u05ea \u05d7\u05d6\u05e8\u05d4" } } diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index c6f3fdb8ce3..c660ca15e87 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -9,7 +9,8 @@ }, "step": { "reauth_confirm": { - "description": "L'int\u00e9gration SmartTub doit r\u00e9-authentifier votre compte" + "description": "L'int\u00e9gration SmartTub doit r\u00e9-authentifier votre compte", + "title": "R\u00e9authentification de l'int\u00e9gration" }, "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json index d88d1320279..9fc1af6d92c 100644 --- a/homeassistant/components/somfy_mylink/translations/de.json +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -49,5 +49,5 @@ } } }, - "title": "" + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/fr.json b/homeassistant/components/sonos/translations/fr.json index 2bae0a69826..50a6086e2e8 100644 --- a/homeassistant/components/sonos/translations/fr.json +++ b/homeassistant/components/sonos/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Aucun p\u00e9riph\u00e9rique Sonos trouv\u00e9 sur le r\u00e9seau.", + "not_sonos_device": "L'appareil d\u00e9couvert n'est pas un appareil Sonos", "single_instance_allowed": "Une seule configuration de Sonos est n\u00e9cessaire." }, "step": { diff --git a/homeassistant/components/sonos/translations/he.json b/homeassistant/components/sonos/translations/he.json index 91cbe81a2a6..878c14a5119 100644 --- a/homeassistant/components/sonos/translations/he.json +++ b/homeassistant/components/sonos/translations/he.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 Sonos \u05d1\u05e8\u05e9\u05ea.", - "single_instance_allowed": "\u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Sonos \u05e0\u05d7\u05d5\u05e6\u05d4." + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { "confirm": { diff --git a/homeassistant/components/switcher_kis/translations/fr.json b/homeassistant/components/switcher_kis/translations/fr.json index 059be87fc07..e6e7a3c271f 100644 --- a/homeassistant/components/switcher_kis/translations/fr.json +++ b/homeassistant/components/switcher_kis/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "step": { "confirm": { diff --git a/homeassistant/components/switcher_kis/translations/it.json b/homeassistant/components/switcher_kis/translations/it.json new file mode 100644 index 00000000000..0278fe07bfe --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/fr.json b/homeassistant/components/syncthing/translations/fr.json new file mode 100644 index 00000000000..12486fb5cf2 --- /dev/null +++ b/homeassistant/components/syncthing/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte" + }, + "step": { + "user": { + "data": { + "title": "Configurer l'int\u00e9gration de Syncthing", + "token": "Jeton", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "title": "Synchroniser" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json index a9fdd199618..561c6c97e0b 100644 --- a/homeassistant/components/synology_dsm/translations/cs.json +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -28,6 +29,13 @@ "description": "Chcete nastavit {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Synology DSM Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index ba4d0a88a6b..b254fc8e561 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "H\u00f4te d\u00e9j\u00e0 configur\u00e9" + "already_configured": "H\u00f4te d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -34,7 +35,8 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Raison: {details}" + "description": "Raison: {details}", + "title": "Synology DSM R\u00e9-authentifier l'int\u00e9gration" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json index a671684a770..4d95d5c2c3c 100644 --- a/homeassistant/components/synology_dsm/translations/he.json +++ b/homeassistant/components/synology_dsm/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -19,7 +20,13 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" }, - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" + }, + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index 6f5fd4ac245..bb6965255bb 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -29,6 +30,14 @@ "description": "Vuoi impostare {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Motivo: {details}", + "title": "Synology DSM Autenticare nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json index 187360bac5e..a21fab81777 100644 --- a/homeassistant/components/system_bridge/translations/fr.json +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" }, "flow_title": "Pont syst\u00e8me: {name}", "step": { diff --git a/homeassistant/components/system_health/translations/he.json b/homeassistant/components/system_health/translations/he.json index 2c46fb48c7d..ae5bdc2388a 100644 --- a/homeassistant/components/system_health/translations/he.json +++ b/homeassistant/components/system_health/translations/he.json @@ -1,3 +1,3 @@ { - "title": "\u05d1\u05e8\u05d9\u05d0\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea" + "title": "\u05d1\u05e8\u05d9\u05d0\u05d5\u05ea \u05d4\u05de\u05e2\u05e8\u05db\u05ea" } \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/ca.json b/homeassistant/components/tesla/translations/ca.json index 2a51c0297ae..f5c0117f6a0 100644 --- a/homeassistant/components/tesla/translations/ca.json +++ b/homeassistant/components/tesla/translations/ca.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Codi MFA (opcional)", "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index bdcd8237b3b..09934369f6b 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA-Code (optional)", "password": "Passwort", "username": "E-Mail" }, diff --git a/homeassistant/components/tesla/translations/et.json b/homeassistant/components/tesla/translations/et.json index c7ceae36990..ab36a4e503d 100644 --- a/homeassistant/components/tesla/translations/et.json +++ b/homeassistant/components/tesla/translations/et.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA kood (valikuline)", "password": "Salas\u00f5na", "username": "E-post" }, diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json index 889c32a7d91..174b687f26f 100644 --- a/homeassistant/components/tesla/translations/fr.json +++ b/homeassistant/components/tesla/translations/fr.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Code MFA (facultatif)", "password": "Mot de passe", "username": "Email" }, diff --git a/homeassistant/components/tesla/translations/it.json b/homeassistant/components/tesla/translations/it.json index 3a137da78f1..05a663df149 100644 --- a/homeassistant/components/tesla/translations/it.json +++ b/homeassistant/components/tesla/translations/it.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Codice autenticazione a pi\u00f9 fattori MFA (facoltativo)", "password": "Password", "username": "E-mail" }, diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json index 5655a641f96..689766cd906 100644 --- a/homeassistant/components/tesla/translations/nl.json +++ b/homeassistant/components/tesla/translations/nl.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA Code (optioneel)", "password": "Wachtwoord", "username": "E-mail" }, diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json index 7ec634cd56c..266a0e82dbe 100644 --- a/homeassistant/components/tesla/translations/pl.json +++ b/homeassistant/components/tesla/translations/pl.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Kod uwierzytelniania wielosk\u0142adnikowego (opcjonalnie)", "password": "Has\u0142o", "username": "Adres e-mail" }, diff --git a/homeassistant/components/tesla/translations/ru.json b/homeassistant/components/tesla/translations/ru.json index d62a2e1f168..191d10b8bea 100644 --- a/homeassistant/components/tesla/translations/ru.json +++ b/homeassistant/components/tesla/translations/ru.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "\u041a\u043e\u0434 MFA (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, diff --git a/homeassistant/components/tesla/translations/zh-Hant.json b/homeassistant/components/tesla/translations/zh-Hant.json index d9b7fd4ef79..9ff407efaa3 100644 --- a/homeassistant/components/tesla/translations/zh-Hant.json +++ b/homeassistant/components/tesla/translations/zh-Hant.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA \u78bc\uff08\u9078\u9805\uff09", "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6" }, diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index b46bf127963..668b20726fc 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -11,7 +11,8 @@ "step": { "locations": { "data": { - "location": "Emplacement" + "location": "Emplacement", + "usercode": "Code d'utilisateur" }, "description": "Saisissez le code d'utilisateur de cet utilisateur \u00e0 cet emplacement", "title": "Codes d'utilisateur de l'emplacement" diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index 55d53f6e676..888c65226dc 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9 TP-Link \u05d1\u05e8\u05e9\u05ea.", - "single_instance_allowed": "\u05e0\u05d3\u05e8\u05e9\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d1\u05dc\u05d1\u05d3" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { "confirm": { diff --git a/homeassistant/components/traccar/translations/de.json b/homeassistant/components/traccar/translations/de.json index 7e253c1d05f..3e94aaeb4c5 100644 --- a/homeassistant/components/traccar/translations/de.json +++ b/homeassistant/components/traccar/translations/de.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/he.json b/homeassistant/components/unifi/translations/he.json index 83c34cb9c77..4fe52a3cf8b 100644 --- a/homeassistant/components/unifi/translations/he.json +++ b/homeassistant/components/unifi/translations/he.json @@ -33,6 +33,14 @@ "track_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 \u05e8\u05e9\u05ea (\u05d4\u05ea\u05e7\u05e0\u05d9 Ubiquiti)" } }, + "init": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + } + }, "simple_options": { "data": { "block_client": "\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05de\u05d1\u05d5\u05e7\u05e8\u05d9\u05dd \u05e9\u05dc \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e8\u05e9\u05ea", diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json index 86e4d7409cf..7c0454e7707 100644 --- a/homeassistant/components/upb/translations/de.json +++ b/homeassistant/components/upb/translations/de.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.", + "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index fe1f1366d39..ffbef69abe7 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -26,5 +26,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle de mise \u00e0 jour (secondes, minimum 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json index 706d87f0db4..e9aba0a7a58 100644 --- a/homeassistant/components/upnp/translations/he.json +++ b/homeassistant/components/upnp/translations/he.json @@ -12,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "init": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "user": { "data": { "unique_id": "\u05d4\u05ea\u05e7\u05df", diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json new file mode 100644 index 00000000000..04428ef567f --- /dev/null +++ b/homeassistant/components/wallbox/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "station": "Num\u00e9ro de s\u00e9rie de la station", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/fr.json b/homeassistant/components/wemo/translations/fr.json index ccf2ac6ef21..e0372147f58 100644 --- a/homeassistant/components/wemo/translations/fr.json +++ b/homeassistant/components/wemo/translations/fr.json @@ -9,5 +9,10 @@ "description": "Voulez-vous configurer Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Le bouton Wemo a \u00e9t\u00e9 enfonc\u00e9 pendant 2 secondes" + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/fr.json b/homeassistant/components/wled/translations/fr.json index 137decb7f40..dec038a8a92 100644 --- a/homeassistant/components/wled/translations/fr.json +++ b/homeassistant/components/wled/translations/fr.json @@ -20,5 +20,14 @@ "title": "Dispositif WLED d\u00e9couvert" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Garder la lumi\u00e8re principale, m\u00eame avec 1 segment LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 6b1baf8b0bf..497f559a9de 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -10,7 +10,7 @@ "at_abschaltung": "AT Abschaltung", "at_frostschutz": "AT Frostschutz", "aus": "Aus", - "auto": "", + "auto": "Automatisch", "auto_off_cool": "AutoOffCool", "auto_on_cool": "AutoOnCool", "automatik_aus": "Automatik AUS", diff --git a/homeassistant/components/wolflink/translations/sensor.he.json b/homeassistant/components/wolflink/translations/sensor.he.json index 68b635ba82b..8447fd66b31 100644 --- a/homeassistant/components/wolflink/translations/sensor.he.json +++ b/homeassistant/components/wolflink/translations/sensor.he.json @@ -1,6 +1,7 @@ { "state": { "wolflink__state": { + "permanent": "\u05e7\u05d1\u05d5\u05e2", "solarbetrieb": "\u05de\u05e6\u05d1 \u05e1\u05d5\u05dc\u05d0\u05e8\u05d9", "standby": "\u05de\u05e6\u05d1 \u05d4\u05de\u05ea\u05e0\u05d4", "start": "\u05d4\u05ea\u05d7\u05dc", diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 23a003daa39..17363b347c0 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -13,7 +13,7 @@ "cloud_login_error": "Konnte sich nicht bei Xioami Miio Cloud anmelden, \u00fcberpr\u00fcfe die Anmeldedaten.", "cloud_no_devices": "Keine Ger\u00e4te in diesem Xiaomi Miio Cloud-Konto gefunden.", "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", - "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." + "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 82b2d4991cb..2b68325a246 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -2,10 +2,16 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "Le flux de configuration pour cet appareil Xiaomi Miio est d\u00e9j\u00e0 en cours." + "already_in_progress": "Le flux de configuration pour cet appareil Xiaomi Miio est d\u00e9j\u00e0 en cours.", + "incomplete_info": "Informations incompl\u00e8tes pour configurer l'appareil, aucun h\u00f4te ou jeton fourni.", + "not_xiaomi_miio": "L'appareil n'est pas (encore) pris en charge par Xiaomi Miio.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "\u00c9chec de connexion", + "cloud_credentials_incomplete": "Identifiants cloud incomplets, veuillez renseigner le nom d'utilisateur, le mot de passe et le pays", + "cloud_login_error": "Impossible de se connecter \u00e0 Xioami Miio Cloud, v\u00e9rifiez les informations d'identification.", + "cloud_no_devices": "Aucun appareil trouv\u00e9 dans ce compte cloud Xiaomi Miio.", "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil.", "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration." }, @@ -13,14 +19,20 @@ "step": { "cloud": { "data": { + "cloud_country": "Pays du serveur cloud", "cloud_password": "Mot de passe cloud", - "cloud_username": "Nom d'utilisateur cloud" - } + "cloud_username": "Nom d'utilisateur cloud", + "manual": "Configurer manuellement (non recommand\u00e9)" + }, + "description": "Connectez-vous au cloud Xiaomi Miio, voir https://www.openhab.org/addons/bindings/miio/#country-servers pour le serveur cloud \u00e0 utiliser.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" }, "connect": { "data": { "model": "Mod\u00e8le d'appareil" - } + }, + "description": "S\u00e9lectionner manuellement le mod\u00e8le d'appareil parmi les mod\u00e8les pris en charge.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" }, "device": { "data": { @@ -41,10 +53,24 @@ "description": "Vous aurez besoin du jeton API, voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions.", "title": "Se connecter \u00e0 la passerelle Xiaomi" }, + "manual": { + "data": { + "host": "Adresse IP", + "token": "Jeton API" + }, + "description": "Vous aurez besoin du jeton API de 32 caract\u00e8res, voir https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token pour les instructions. Veuillez noter que ce jeton API est diff\u00e9rent de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration de Xiaomi Miio doit r\u00e9-authentifier votre compte afin de mettre \u00e0 jour les jetons ou d'ajouter les informations d'identification cloud manquantes.", + "title": "R\u00e9authentification de l'int\u00e9gration" + }, "select": { "data": { "select_device": "Appareil Miio" - } + }, + "description": "S\u00e9lectionner l'appareil Xiaomi Miio \u00e0 configurer.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 36f54e79361..e3bf59f9459 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -58,7 +58,7 @@ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" }, - "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", + "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" }, "reauth_confirm": { diff --git a/homeassistant/components/yale_smart_alarm/translations/ca.json b/homeassistant/components/yale_smart_alarm/translations/ca.json index f3865103996..ab77170999b 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ca.json +++ b/homeassistant/components/yale_smart_alarm/translations/ca.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID d'\u00e0rea", + "name": "Nom", "password": "Contrasenya", "username": "Nom d'usuari" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID d'\u00e0rea", + "name": "Nom", "password": "Contrasenya", "username": "Nom d'usuari" } diff --git a/homeassistant/components/yale_smart_alarm/translations/cs.json b/homeassistant/components/yale_smart_alarm/translations/cs.json new file mode 100644 index 00000000000..f19158bca25 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/en.json b/homeassistant/components/yale_smart_alarm/translations/en.json index 84cfb893ad5..a439971fb3f 100644 --- a/homeassistant/components/yale_smart_alarm/translations/en.json +++ b/homeassistant/components/yale_smart_alarm/translations/en.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "Area ID", + "name": "Name", "password": "Password", "username": "Username" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "Area ID", + "name": "Name", "password": "Password", "username": "Username" } diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json new file mode 100644 index 00000000000..60d6f5cc548 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification incorrecte" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID de la zone", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, + "user": { + "data": { + "area_id": "ID de la zone", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/he.json b/homeassistant/components/yale_smart_alarm/translations/he.json new file mode 100644 index 00000000000..41f5d4493bf --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "\u05de\u05d6\u05d4\u05d4 \u05d0\u05d6\u05d5\u05e8", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "area_id": "\u05de\u05d6\u05d4\u05d4 \u05d0\u05d6\u05d5\u05e8", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/it.json b/homeassistant/components/yale_smart_alarm/translations/it.json new file mode 100644 index 00000000000..2f510e46396 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID area", + "name": "Nome", + "password": "Password", + "username": "Nome utente" + } + }, + "user": { + "data": { + "area_id": "ID area", + "name": "Nome", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/nl.json b/homeassistant/components/yale_smart_alarm/translations/nl.json new file mode 100644 index 00000000000..53c1b8fb086 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Area ID", + "name": "Naam", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, + "user": { + "data": { + "area_id": "Area ID", + "name": "Naam", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/no.json b/homeassistant/components/yale_smart_alarm/translations/no.json new file mode 100644 index 00000000000..bbeedb7dc89 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "name": "Navn", + "password": "Passord", + "username": "Brukernavn" + } + }, + "user": { + "data": { + "name": "Navn", + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/pl.json b/homeassistant/components/yale_smart_alarm/translations/pl.json new file mode 100644 index 00000000000..553d05ee439 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Identyfikator obszaru", + "name": "Nazwa", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "area_id": "Identyfikator obszaru", + "name": "[%key::common::config_flow::data::name%]", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ru.json b/homeassistant/components/yale_smart_alarm/translations/ru.json index 03af44dd983..aedf07d030e 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ru.json +++ b/homeassistant/components/yale_smart_alarm/translations/ru.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } diff --git a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json index 6cfcaf7c83b..e02b74f27a1 100644 --- a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json +++ b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "\u5206\u5340 ID", + "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "\u5206\u5340 ID", + "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } diff --git a/homeassistant/components/yamaha_musiccast/translations/fr.json b/homeassistant/components/yamaha_musiccast/translations/fr.json new file mode 100644 index 00000000000..0a8671dc2aa --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "yxc_control_url_missing": "L'URL de contr\u00f4le n'est pas donn\u00e9e dans la description ssdp." + }, + "error": { + "no_musiccast_device": "Cet appareil ne semble pas \u00eatre un appareil MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurer MusicCast pour l'int\u00e9grer \u00e0 Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index be55fe57ee0..9682f0d9b6f 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer {model} ({host})\u00a0?" + }, "pick_device": { "data": { "device": "Appareil" diff --git a/homeassistant/components/youless/translations/ca.json b/homeassistant/components/youless/translations/ca.json new file mode 100644 index 00000000000..1237597b797 --- /dev/null +++ b/homeassistant/components/youless/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/cs.json b/homeassistant/components/youless/translations/cs.json new file mode 100644 index 00000000000..7a27355056b --- /dev/null +++ b/homeassistant/components/youless/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/de.json b/homeassistant/components/youless/translations/de.json new file mode 100644 index 00000000000..a87bbe1aa46 --- /dev/null +++ b/homeassistant/components/youless/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/en.json b/homeassistant/components/youless/translations/en.json index 38923682b10..584a8283675 100644 --- a/homeassistant/components/youless/translations/en.json +++ b/homeassistant/components/youless/translations/en.json @@ -1,21 +1,15 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect" }, "step": { "user": { "data": { "host": "Host", - "password": "Password", - "username": "Username" + "name": "Name" } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/et.json b/homeassistant/components/youless/translations/et.json new file mode 100644 index 00000000000..9a26513c333 --- /dev/null +++ b/homeassistant/components/youless/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/fr.json b/homeassistant/components/youless/translations/fr.json new file mode 100644 index 00000000000..6f9c76d9ba1 --- /dev/null +++ b/homeassistant/components/youless/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/he.json b/homeassistant/components/youless/translations/he.json new file mode 100644 index 00000000000..33660936e12 --- /dev/null +++ b/homeassistant/components/youless/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/it.json b/homeassistant/components/youless/translations/it.json new file mode 100644 index 00000000000..8f93107a15d --- /dev/null +++ b/homeassistant/components/youless/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/nl.json b/homeassistant/components/youless/translations/nl.json new file mode 100644 index 00000000000..a05a1e161cc --- /dev/null +++ b/homeassistant/components/youless/translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/no.json b/homeassistant/components/youless/translations/no.json new file mode 100644 index 00000000000..01ea5b65fb1 --- /dev/null +++ b/homeassistant/components/youless/translations/no.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Navn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/pl.json b/homeassistant/components/youless/translations/pl.json new file mode 100644 index 00000000000..98acbf5ef4b --- /dev/null +++ b/homeassistant/components/youless/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/ru.json b/homeassistant/components/youless/translations/ru.json new file mode 100644 index 00000000000..341b6a603aa --- /dev/null +++ b/homeassistant/components/youless/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/zh-Hant.json b/homeassistant/components/youless/translations/zh-Hant.json new file mode 100644 index 00000000000..9ba777cefba --- /dev/null +++ b/homeassistant/components/youless/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 75ba26ca809..90d0908d6c3 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -41,6 +41,8 @@ "title": "Options du panneau de contr\u00f4le d'alarme" }, "zha_options": { + "consider_unavailable_battery": "Consid\u00e9rer les appareils aliment\u00e9s par batterie indisponibles apr\u00e8s (secondes)", + "consider_unavailable_mains": "Consid\u00e9rer les appareils aliment\u00e9s par le secteur indisponibles apr\u00e8s (secondes)", "default_light_transition": "Temps de transition de la lumi\u00e8re par d\u00e9faut (en secondes)", "enable_identify_on_join": "Activer l'effet d'identification quand les appareils rejoignent le r\u00e9seau", "title": "Options g\u00e9n\u00e9rales" diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index b226a2e51e0..9d432edd1e5 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -13,7 +13,7 @@ "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", "usb_path": "USB-Ger\u00e4te-Pfad" }, - "description": "Diese Integration wird nicht mehr gepflegt. Verwenden Sie bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" + "description": "Diese Integration wird nicht mehr gepflegt. Verwende bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" } } }, diff --git a/homeassistant/components/zwave/translations/he.json b/homeassistant/components/zwave/translations/he.json index 585b696b496..9cbf39a6d16 100644 --- a/homeassistant/components/zwave/translations/he.json +++ b/homeassistant/components/zwave/translations/he.json @@ -15,13 +15,13 @@ "state": { "_": { "dead": "\u05de\u05ea", - "initializing": "\u05de\u05d0\u05ea\u05d7\u05dc", + "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc", "ready": "\u05de\u05d5\u05db\u05df", "sleeping": "\u05d9\u05e9\u05df" }, "query_stage": { - "dead": "\u05de\u05ea ({query_stage})", - "initializing": "\u05de\u05d0\u05ea\u05d7\u05dc ({query_stage})" + "dead": "\u05de\u05ea", + "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 6435453b1df..9b01865d3be 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -116,5 +116,5 @@ } } }, - "title": "" + "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 46737b8c79d..1e51e97044a 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -52,23 +52,67 @@ } }, "device_automation": { + "condition_type": { + "config_parameter": "Valeur du param\u00e8tre de configuration {subtype}", + "node_status": "\u00c9tat du n\u0153ud", + "value": "Valeur actuelle d'une valeur Z-Wave" + }, "trigger_type": { + "event.notification.entry_control": "Envoi d'une notification de contr\u00f4le d'entr\u00e9e", "event.notification.notification": "Envoyer une notification", + "event.value_notification.basic": "\u00c9v\u00e9nement CC de base sur {subtype}", + "event.value_notification.central_scene": "Action de la sc\u00e8ne centrale sur {subtype}", "event.value_notification.scene_activation": "Activation de la sc\u00e8ne sur {sous-type}", "state.node_status": "Changement de statut du noeud" } }, "options": { + "abort": { + "addon_get_discovery_info_failed": "\u00c9chec de l'obtention des informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", + "addon_info_failed": "\u00c9chec de l'obtention des informations sur le module compl\u00e9mentaire Z-Wave JS.", + "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.", + "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", + "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "different_device": "Le p\u00e9riph\u00e9rique USB connect\u00e9 n'est pas le m\u00eame que pr\u00e9c\u00e9demment configur\u00e9 pour cette entr\u00e9e de configuration. Veuillez plut\u00f4t cr\u00e9er une nouvelle entr\u00e9e de configuration pour le nouveau p\u00e9riph\u00e9rique." + }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_ws_url": "URL websocket invalide" + "invalid_ws_url": "URL websocket invalide", + "unknown": "Erreur inattendue" + }, + "progress": { + "install_addon": "Veuillez patienter pendant que l'installation du module compl\u00e9mentaire Z-Wave JS se termine. Cela peut prendre plusieurs minutes.", + "start_addon": "Veuillez patienter pendant que le d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS se termine. Cela peut prendre quelques secondes." }, "step": { "configure_addon": { "data": { + "emulate_hardware": "\u00c9muler le mat\u00e9riel", "log_level": "Niveau du journal", - "network_key": "Cl\u00e9 r\u00e9seau" + "network_key": "Cl\u00e9 r\u00e9seau", + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "title": "Entrer dans la configuration du module compl\u00e9mentaire Z-Wave JS" + }, + "install_addon": { + "title": "L'installation du module compl\u00e9mentaire Z-Wave JS a commenc\u00e9" + }, + "manual": { + "data": { + "url": "URL" } + }, + "on_supervisor": { + "data": { + "use_addon": "Utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor" + }, + "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor\u00a0?", + "title": "S\u00e9lectionner la m\u00e9thode de connexion" + }, + "start_addon": { + "title": "Le module compl\u00e9mentaire Z-Wave JS d\u00e9marre." } } }, From 9cee9d9d8aa7c2e5c7316cb1f80374a72a725525 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 1 Aug 2021 20:35:03 -0700 Subject: [PATCH 780/818] Add energy consumption sensors to smartthings devices (#53759) --- .../components/smartthings/sensor.py | 62 +++++++++++++++++++ tests/components/smartthings/test_sensor.py | 49 +++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 7a7f9a51855..5059bcc4403 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -220,6 +220,7 @@ CAPABILITY_TO_SENSORS = { Capability.oven_setpoint: [ Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None) ], + Capability.power_consumption_report: [], Capability.power_meter: [ Map( Attribute.power, @@ -388,6 +389,13 @@ CAPABILITY_TO_SENSORS = { UNITS = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT} THREE_AXIS_NAMES = ["X Coordinate", "Y Coordinate", "Z Coordinate"] +POWER_CONSUMPTION_REPORT_NAMES = [ + "energy", + "power", + "deltaEnergy", + "powerEnergy", + "energySaved", +] async def async_setup_entry(hass, config_entry, async_add_entities): @@ -403,6 +411,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for index in range(len(THREE_AXIS_NAMES)) ] ) + elif capability == Capability.power_consumption_report: + sensors.extend( + [ + SmartThingsPowerConsumptionSensor(device, report_name) + for report_name in POWER_CONSUMPTION_REPORT_NAMES + ] + ) else: maps = CAPABILITY_TO_SENSORS[capability] sensors.extend( @@ -526,3 +541,50 @@ class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): return three_axis[self._index] except (TypeError, IndexError): return None + + +class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): + """Define a SmartThings Sensor.""" + + def __init__( + self, + device: DeviceEntity, + report_name: str, + ) -> None: + """Init the class.""" + super().__init__(device) + self.report_name = report_name + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return f"{self._device.label} {self.report_name}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._device.device_id}.{self.report_name}" + + @property + def state(self): + """Return the state of the sensor.""" + value = self._device.status.attributes[Attribute.power_consumption].value + if value.get(self.report_name) is None: + return None + if self.report_name == "power": + return value[self.report_name] + return value[self.report_name] / 1000 + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.report_name == "power": + return DEVICE_CLASS_POWER + return DEVICE_CLASS_ENERGY + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + if self.report_name == "power": + return POWER_WATT + return ENERGY_KILO_WATT_HOUR diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index ffb577c903a..fa849a3cc67 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -139,6 +139,55 @@ async def test_energy_sensors_for_switch_device(hass, device_factory): assert entry.manufacturer == "Unavailable" +async def test_power_consumption_sensor(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory( + "refrigerator", + [Capability.power_consumption_report], + { + Attribute.power_consumption: { + "energy": 1412002, + "deltaEnergy": 25, + "power": 109, + "powerEnergy": 24.304498331745464, + "persistedEnergy": 0, + "energySaved": 0, + "start": "2021-07-30T16:45:25Z", + "end": "2021-07-30T16:58:33Z", + } + }, + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + # Act + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + # Assert + state = hass.states.get("sensor.refrigerator_energy") + assert state + assert state.state == "1412.002" + entry = entity_registry.async_get("sensor.refrigerator_energy") + assert entry + assert entry.unique_id == f"{device.device_id}.energy" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + state = hass.states.get("sensor.refrigerator_power") + assert state + assert state.state == "109" + entry = entity_registry.async_get("sensor.refrigerator_power") + assert entry + assert entry.unique_id == f"{device.device_id}.power" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + async def test_update_from_signal(hass, device_factory): """Test the binary_sensor updates when receiving a signal.""" # Arrange From 3d6ba793f73f3eabe681f445236f929fd25fd4f2 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 2 Aug 2021 04:59:32 +0100 Subject: [PATCH 781/818] Fix error in homekit_controller causing some entities to get an incorrect unique id (#53848) --- .../components/homekit_controller/__init__.py | 7 +- .../components/homekit_controller/number.py | 3 +- .../components/homekit_controller/sensor.py | 3 +- .../specific_devices/test_ecobee3.py | 2 +- .../specific_devices/test_koogeek_p1eu.py | 2 +- .../specific_devices/test_koogeek_sw2.py | 2 +- .../specific_devices/test_mysa_living.py | 91 +++++++ .../test_vocolinc_flowerbud.py | 4 +- .../homekit_controller/mysa_living.json | 250 ++++++++++++++++++ 9 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_mysa_living.py create mode 100644 tests/fixtures/homekit_controller/mysa_living.json diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 404b2f54ab0..c14cfbb8a7e 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -189,11 +189,16 @@ class CharacteristicEntity(HomeKitEntity): the service entity. """ + def __init__(self, accessory, devinfo, char): + """Initialise a generic single characteristic HomeKit entity.""" + self._char = char + super().__init__(accessory, devinfo) + @property def unique_id(self) -> str: """Return the ID of this device.""" serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-aid:{self._aid}-sid:{self._iid}-cid:{self._iid}" + return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 2e0193fa080..73d8cd6adbd 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -53,9 +53,8 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): self._device_class = device_class self._icon = icon self._name = name - self._char = char - super().__init__(conn, info) + super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index f34fc3f0a9b..6bf8a7fc084 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -254,9 +254,8 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): self._unit = unit self._icon = icon self._name = name - self._char = char - super().__init__(conn, info) + super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 96dbd3b0718..94f3aabc12a 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -60,7 +60,7 @@ async def test_ecobee3_setup(hass): assert climate_state.attributes["max_humidity"] == 50 climate_sensor = entity_registry.async_get("sensor.homew_current_temperature") - assert climate_sensor.unique_id == "homekit-123456789012-aid:1-sid:16-cid:16" + assert climate_sensor.unique_id == "homekit-123456789012-aid:1-sid:16-cid:19" occ1 = entity_registry.async_get("binary_sensor.kitchen") assert occ1.unique_id == "homekit-AB1C-56" diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index db72aad7541..6302013223d 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -37,7 +37,7 @@ async def test_koogeek_p1eu_setup(hass): # Assert the power sensor is detected entry = entity_registry.async_get("sensor.koogeek_p1_a00aa0_real_time_energy") - assert entry.unique_id == "homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:21" + assert entry.unique_id == "homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:22" helper = Helper( hass, diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 00057822071..1d46b633153 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -45,7 +45,7 @@ async def test_koogeek_ls1_setup(hass): # Assert that the power sensor entity is correctly added to the entity registry entry = entity_registry.async_get("sensor.koogeek_sw2_187a91_real_time_energy") - assert entry.unique_id == "homekit-CNNT061751001372-aid:1-sid:14-cid:14" + assert entry.unique_id == "homekit-CNNT061751001372-aid:1-sid:14-cid:18" helper = Helper( hass, diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py new file mode 100644 index 00000000000..ea1c1084071 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -0,0 +1,91 @@ +"""Make sure that Mysa Living is enumerated properly.""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_mysa_living_setup(hass): + """Test that the accessory can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mysa_living.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Check that the switch entity is handled correctly + + entry = entity_registry.async_get("sensor.mysa_85dda9_current_humidity") + assert entry.unique_id == "homekit-AAAAAAA000-aid:1-sid:20-cid:27" + + helper = Helper( + hass, + "sensor.mysa_85dda9_current_humidity", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9 - Current Humidity" + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Empowered Homes Inc." + assert device.name == "Mysa-85dda9" + assert device.model == "v1" + assert device.sw_version == "2.8.1" + assert device.via_device_id is None + + # Assert the humidifier is detected + entry = entity_registry.async_get("sensor.mysa_85dda9_current_temperature") + assert entry.unique_id == "homekit-AAAAAAA000-aid:1-sid:20-cid:25" + + helper = Helper( + hass, + "sensor.mysa_85dda9_current_temperature", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9 - Current Temperature" + + # The sensor should be part of the same device + assert entry.device_id == device.id + + # Assert the light is detected + entry = entity_registry.async_get("light.mysa_85dda9") + assert entry.unique_id == "homekit-AAAAAAA000-40" + + helper = Helper( + hass, + "light.mysa_85dda9", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9" + + # The light should be part of the same device + assert entry.device_id == device.id + + # Assert the climate entity is detected + entry = entity_registry.async_get("climate.mysa_85dda9") + assert entry.unique_id == "homekit-AAAAAAA000-20" + + helper = Helper( + hass, + "climate.mysa_85dda9", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9" + + # The light should be part of the same device + assert entry.device_id == device.id diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index e2762b5c153..6968c62257f 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -20,7 +20,7 @@ async def test_vocolinc_flowerbud_setup(hass): # Check that the switch entity is handled correctly entry = entity_registry.async_get("number.vocolinc_flowerbud_0d324b") - assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:30" + assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:38" helper = Helper( hass, "number.vocolinc_flowerbud_0d324b", pairing, accessories[0], config_entry @@ -73,7 +73,7 @@ async def test_vocolinc_flowerbud_setup(hass): entry = entity_registry.async_get( "sensor.vocolinc_flowerbud_0d324b_current_humidity" ) - assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:30" + assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:33" helper = Helper( hass, diff --git a/tests/fixtures/homekit_controller/mysa_living.json b/tests/fixtures/homekit_controller/mysa_living.json new file mode 100644 index 00000000000..da26b654fe5 --- /dev/null +++ b/tests/fixtures/homekit_controller/mysa_living.json @@ -0,0 +1,250 @@ +[ + { + "aid": 1, + "primary": true, + "services": [ + { + "type": "0000004A-0000-1000-8000-0026BB765291", + "primary": true, + "iid": 20, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Thermostat", + "perms": [ + "pr" + ], + "iid": 24 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "format": "float", + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "value": 40, + "iid": 27, + "unit": "percentage", + "perms": [ + "pr", + "ev" + ] + }, + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "value": 0, + "minValue": 0, + "maxValue": 2, + "stepValue": 1, + "format": "uint8", + "perms": [ + "pr", + "ev" + ], + "iid": 21 + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "value": 0, + "minValue": 0, + "maxValue": 3, + "stepValue": 1, + "format": "uint8", + "perms": [ + "pr", + "pw", + "ev" + ], + "iid": 22 + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "value": 24.1, + "minValue": 0, + "maxValue": 100, + "stepValue": 0.1, + "unit": "celsius", + "format": "float", + "perms": [ + "pr", + "ev" + ], + "iid": 25 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "value": 22, + "minValue": 5, + "maxValue": 30, + "stepValue": 0.1, + "unit": "celsius", + "format": "float", + "perms": [ + "pr", + "pw", + "ev" + ], + "iid": 23 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "format": "uint8", + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "value": 0, + "iid": 26, + "perms": [ + "pr", + "pw", + "ev" + ] + } + ] + }, + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "iid": 2, + "format": "bool" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Empowered Homes Inc.", + "perms": [ + "pr" + ], + "iid": 3 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "v1", + "perms": [ + "pr" + ], + "iid": 4 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Mysa-85dda9", + "perms": [ + "pr" + ], + "iid": 5 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "AAAAAAA000", + "perms": [ + "pr" + ], + "iid": 6 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.8.1", + "perms": [ + "pr" + ], + "iid": 7 + }, + { + "hidden": true, + "type": "22280E2C-9B79-43BD-8370-5A8F67777B29", + "format": "string", + "value": "b4e62d85dda9", + "perms": [ + "pr" + ], + "iid": 8 + } + ] + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 10, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": [ + "pr" + ], + "iid": 11 + } + ] + }, + { + "type": "00000043-0000-1000-8000-0026BB765291", + "iid": 40, + "characteristics": [ + { + "type": "00000025-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "iid": 42 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Display", + "perms": [ + "pr" + ], + "iid": 41 + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "format": "int", + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "value": 0, + "iid": 43, + "unit": "percentage", + "perms": [ + "pr", + "pw", + "ev" + ] + } + ] + }, + { + "type": "3354EC82-AF38-4755-B4A4-4DB8E418F555", + "iid": 50, + "characteristics": [ + { + "hidden": true, + "type": "E71D8348-BB33-4C34-8C50-A64B1136EDD2", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw" + ], + "iid": 51 + } + ] + } + ] + } +] \ No newline at end of file From ab4ed128cc7f1f9bb815c6f8a273efff215d2bc2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Aug 2021 21:00:14 -0700 Subject: [PATCH 782/818] Bumped version to 2021.8.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 974fcd01b7b..0afc18f43bb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 1c30967f6fd3eedc86154f6bd0bb1d60bad88ffb Mon Sep 17 00:00:00 2001 From: Vinny Furia Date: Mon, 2 Aug 2021 06:57:10 -0600 Subject: [PATCH 783/818] Fix Radiothermostat hold value updates (#53656) --- homeassistant/components/radiotherm/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index aad6bf3989e..fc108af56a7 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -80,6 +80,8 @@ PRESET_MODE_TO_CODE = {"home": 0, "alternate": 1, "away": 2, "holiday": 3} CODE_TO_PRESET_MODE = {0: "home", 1: "alternate", 2: "away", 3: "holiday"} +CODE_TO_HOLD_STATE = {0: False, 1: True} + def round_temp(temperature): """Round a temperature to the resolution of the thermostat. @@ -300,6 +302,7 @@ class RadioThermostat(ClimateEntity): self._fstate = CODE_TO_FAN_STATE[data["fstate"]] self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] + self._hold_set = CODE_TO_HOLD_STATE[data["hold"]] self._current_operation = self._tmode if self._tmode == HVAC_MODE_COOL: From 1a1efecdba149899a19999205454b41ac687f806 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Mon, 2 Aug 2021 14:54:33 +0200 Subject: [PATCH 784/818] Fix missing default reconnect interval in dsmr (#53760) --- homeassistant/components/dsmr/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 5afc229a727..faff62ddeb4 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -157,7 +157,9 @@ async def async_setup_entry( update_entities_telegram({}) # throttle reconnect attempts - await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) + await asyncio.sleep( + entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL) + ) except (serial.serialutil.SerialException, OSError): # Log any error while establishing connection and drop to retry @@ -167,7 +169,9 @@ async def async_setup_entry( protocol = None # throttle reconnect attempts - await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) + await asyncio.sleep( + entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL) + ) except CancelledError: if stop_listener: stop_listener() # pylint: disable=not-callable From 91af3b0502618e518ec08fae286f0a06ca74f288 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 2 Aug 2021 09:59:23 -0300 Subject: [PATCH 785/818] Fix entry setup for Broadlink SP4 sensors (#53765) --- homeassistant/components/broadlink/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index f73f669326d..30bc8047d03 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -69,7 +69,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [ BroadlinkSensor(device, monitored_condition) for monitored_condition in sensor_data - if sensor_data[monitored_condition] != 0 or device.api.type == "A1" + if monitored_condition in SENSOR_TYPES + and ( + # These devices have optional sensors. + # We don't create entities if the value is 0. + sensor_data[monitored_condition] != 0 + or device.api.type not in {"RM4PRO", "RM4MINI"} + ) ] async_add_entities(sensors) From 31af17f7f741493c54783d5d4a5142da2f3e1d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 2 Aug 2021 11:14:45 +0200 Subject: [PATCH 786/818] Bump pysma to 0.6.5 (#53792) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 985a0506574..a462d0c854b 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.4"], + "requirements": ["pysma==0.6.5"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 107a538550c..7453a3cf2c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1760,7 +1760,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.4 +pysma==0.6.5 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea0e298a96b..04c093ad9a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1002,7 +1002,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.4 +pysma==0.6.5 # homeassistant.components.smappee pysmappee==0.2.25 From 2e441d8b7ca8d14e3cdfcec9c143a2c65eab9871 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Aug 2021 18:47:54 +0200 Subject: [PATCH 787/818] Fix TP-Link smart strip devices (#53799) --- homeassistant/components/tplink/__init__.py | 4 +- homeassistant/components/tplink/sensor.py | 4 +- homeassistant/components/tplink/switch.py | 2 +- tests/components/tplink/consts.py | 55 ++++++++++++++------- tests/components/tplink/test_init.py | 23 +++------ 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 88160722669..552e5666db8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -180,9 +180,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: continue hass_data[COORDINATORS][ - switch.mac + switch.context or switch.mac ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) - await coordinator.async_config_entry_first_refresh() if unavailable_devices: @@ -275,4 +274,5 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): except SmartDeviceException as ex: raise UpdateFailed(ex) from ex + self.name = data[CONF_ALIAS] return data diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index bb6596b82d1..697641915f7 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -101,7 +101,9 @@ async def async_setup_entry( ] switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] for switch in switches: - coordinator: SmartPlugDataUpdateCoordinator = coordinators[switch.mac] + coordinator: SmartPlugDataUpdateCoordinator = coordinators[ + switch.context or switch.mac + ] if not switch.has_emeter and coordinator.data.get(CONF_EMETER_PARAMS) is None: continue for description in ENERGY_SENSORS: diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index f5319de999a..10cf5c64d75 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -39,7 +39,7 @@ async def async_setup_entry( ] switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] for switch in switches: - coordinator = coordinators[switch.mac] + coordinator = coordinators[switch.context or switch.mac] entities.append(SmartPlugSwitch(switch, coordinator)) async_add_entities(entities) diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py index 95177a12a9c..e579be61df2 100644 --- a/tests/components/tplink/consts.py +++ b/tests/components/tplink/consts.py @@ -61,31 +61,48 @@ SMARTPLUG_HS100_DATA = { "err_code": 0, } } -SMARTSTRIPWITCH_DATA = { +SMARTSTRIP_KP303_DATA = { "sysinfo": { - "sw_ver": "1.0.4 Build 191111 Rel.143500", - "hw_ver": "4.0", - "model": "HS110(EU)", - "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", - "oemId": "40F54B43071E9436B6395611E9D91CEA", - "hwId": "A6C77E4FDD238B53D824AC8DA361F043", - "rssi": -24, - "longitude_i": 130793, - "latitude_i": 480582, - "alias": "SmartPlug", + "sw_ver": "1.0.4 Build 210428 Rel.135415", + "hw_ver": "1.0", + "model": "KP303(AU)", + "deviceId": "03102547AB1A57A4E4AA5B4EFE34C3005726B97D", + "oemId": "1F950FC9BFF278D9D35E046C129D9411", + "hwId": "9E86D4F840D2787D3D7A6523A731BA2C", + "rssi": -74, + "longitude_i": 1158985, + "latitude_i": -319172, + "alias": "TP-LINK_Power Strip_00B1", "status": "new", "mic_type": "IOT.SMARTPLUGSWITCH", "feature": "TIM", - "mac": "69:F2:3C:8E:E3:47", + "mac": "D4:DD:D6:95:B0:F9", "updating": 0, "led_off": 0, - "relay_state": 0, - "on_time": 0, - "active_mode": "none", - "icon_hash": "", - "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", - "next_action": {"type": -1}, - "children": [{"id": "1", "state": 1, "alias": "SmartPlug#1"}], + "children": [ + { + "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913000", + "state": 0, + "alias": "R-Plug 1", + "on_time": 0, + "next_action": {"type": -1}, + }, + { + "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913001", + "state": 1, + "alias": "R-Plug 2", + "on_time": 93835, + "next_action": {"type": -1}, + }, + { + "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913002", + "state": 1, + "alias": "R-Plug 3", + "on_time": 93834, + "next_action": {"type": -1}, + }, + ], + "child_num": 3, "err_code": 0, }, "realtime": { diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index a201788f35b..d96d6846939 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -20,11 +20,10 @@ from homeassistant.components.tplink.const import ( CONF_LIGHT, CONF_SW_VERSION, CONF_SWITCH, - COORDINATORS, UNAVAILABLE_RETRY_DELAY, ) from homeassistant.components.tplink.sensor import ENERGY_SENSORS -from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -34,7 +33,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro from tests.components.tplink.consts import ( SMARTPLUG_HS100_DATA, SMARTPLUG_HS110_DATA, - SMARTSTRIPWITCH_DATA, + SMARTSTRIP_KP303_DATA, ) @@ -307,7 +306,7 @@ async def test_smartstrip_device(hass: HomeAssistant): """Moked SmartStrip class.""" def get_sysinfo(self): - return SMARTSTRIPWITCH_DATA["sysinfo"] + return SMARTSTRIP_KP303_DATA["sysinfo"] with patch( "homeassistant.components.tplink.common.Discover.discover" @@ -315,9 +314,7 @@ async def test_smartstrip_device(hass: HomeAssistant): "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.common.SmartPlug.get_sysinfo", - return_value=SMARTSTRIPWITCH_DATA["sysinfo"], - ), patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + return_value=SMARTSTRIP_KP303_DATA["sysinfo"], ): strip = SmartStrip("123.123.123.123") @@ -326,16 +323,8 @@ async def test_smartstrip_device(hass: HomeAssistant): assert await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert hass.data.get(tplink.DOMAIN) - assert hass.data[tplink.DOMAIN].get(COORDINATORS) - assert hass.data[tplink.DOMAIN][COORDINATORS].get(strip.mac) - assert isinstance( - hass.data[tplink.DOMAIN][COORDINATORS][strip.mac], - tplink.SmartPlugDataUpdateCoordinator, - ) - data = hass.data[tplink.DOMAIN][COORDINATORS][strip.mac].data - assert data[CONF_ALIAS] == strip.sys_info["children"][0]["alias"] - assert data[CONF_DEVICE_ID] == "1" + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 3 async def test_no_config_creates_no_entry(hass): From 5f8f1ae695467aa40654a9c8292b16013047a797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 2 Aug 2021 11:50:52 +0200 Subject: [PATCH 788/818] Add STATE_CLASS_MEASUREMENT to Tibber (#53802) --- homeassistant/components/tibber/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 3ee1a3749d1..b5012cdc41d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -77,12 +77,14 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="power", name="power", device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), "powerProduction": TibberSensorEntityDescription( key="powerProduction", name="power production", device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), "minPower": TibberSensorEntityDescription( From e9b672c0b4a95b0314238cd87dc12affa1ecd2b3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Aug 2021 16:13:54 +0200 Subject: [PATCH 789/818] Fix crash when AVM FRITZ!SmartHome devices are unreachable (#53809) --- homeassistant/components/fritzbox/__init__.py | 5 +++++ homeassistant/components/fritzbox/binary_sensor.py | 2 -- homeassistant/components/fritzbox/climate.py | 5 ----- homeassistant/components/fritzbox/sensor.py | 8 ++++++-- homeassistant/components/fritzbox/switch.py | 5 ----- tests/components/fritzbox/test_binary_sensor.py | 4 ++-- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 087faeb2be9..cef325a61f3 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -143,6 +143,11 @@ class FritzBoxEntity(CoordinatorEntity): self._device_class = entity_info[ATTR_DEVICE_CLASS] self._attr_state_class = entity_info[ATTR_STATE_CLASS] + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.device.present + @property def device(self) -> FritzhomeDevice: """Return device object from coordinator.""" diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index f6dbaed97cf..5514408cb3c 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -54,6 +54,4 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - if not self.device.present: - return False return self.device.alert_state # type: ignore [no-any-return] diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 0551c5e0455..4baa1b3b81a 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -93,11 +93,6 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """Return the list of supported features.""" return SUPPORT_FLAGS - @property - def available(self) -> bool: - """Return if thermostat is available.""" - return self.device.present # type: ignore [no-any-return] - @property def temperature_unit(self) -> str: """Return the unit of measurement that is used.""" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index d325e592faf..9d78afca4de 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -121,7 +121,9 @@ class FritzBoxPowerSensor(FritzBoxEntity, SensorEntity): @property def state(self) -> float | None: """Return the state of the sensor.""" - return self.device.power / 1000 # type: ignore [no-any-return] + if power := self.device.power: + return power / 1000 # type: ignore [no-any-return] + return 0.0 class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): @@ -130,7 +132,9 @@ class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): @property def state(self) -> float | None: """Return the state of the sensor.""" - return (self.device.energy or 0.0) / 1000 + if energy := self.device.energy: + return energy / 1000 # type: ignore [no-any-return] + return 0.0 @property def last_reset(self) -> datetime: diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 62d1cb1ecf8..133db92feda 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -56,11 +56,6 @@ async def async_setup_entry( class FritzboxSwitch(FritzBoxEntity, SwitchEntity): """The switch class for FRITZ!SmartHome switches.""" - @property - def available(self) -> bool: - """Return if switch is available.""" - return self.device.present # type: ignore [no-any-return] - @property def is_on(self) -> bool: """Return true if the switch is on.""" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index f4e32fbe3df..cb76109e0ff 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -14,8 +14,8 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, - STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -60,7 +60,7 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state - assert state.state == STATE_OFF + assert state.state == STATE_UNAVAILABLE async def test_update(hass: HomeAssistant, fritz: Mock): From d56636ed370f45b416e73b164d32daa4b2ef8926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 2 Aug 2021 18:46:07 +0200 Subject: [PATCH 790/818] Add base energy analytics (#53855) --- .../components/analytics/analytics.py | 11 +++ homeassistant/components/analytics/const.py | 2 + .../components/analytics/manifest.json | 15 +++- homeassistant/components/energy/__init__.py | 7 ++ tests/components/analytics/test_analytics.py | 69 +++++++++++++++++++ tests/components/energy/test_websocket_api.py | 6 +- 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 42630ad2df1..37aff988162 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -8,6 +8,10 @@ import async_timeout from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.energy import ( + DOMAIN as ENERGY_DOMAIN, + is_configured as energy_is_configured, +) from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -26,8 +30,10 @@ from .const import ( ATTR_AUTOMATION_COUNT, ATTR_BASE, ATTR_BOARD, + ATTR_CONFIGURED, ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, + ATTR_ENERGY, ATTR_HEALTHY, ATTR_INTEGRATION_COUNT, ATTR_INTEGRATIONS, @@ -222,6 +228,11 @@ class Analytics: if supervisor_info is not None: payload[ATTR_ADDONS] = addons + if ENERGY_DOMAIN in integrations: + payload[ATTR_ENERGY] = { + ATTR_CONFIGURED: await energy_is_configured(self.hass) + } + if self.preferences.get(ATTR_STATISTICS, False): payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all()) payload[ATTR_AUTOMATION_COUNT] = len( diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 4688c578a00..8576e22073f 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -21,8 +21,10 @@ ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" ATTR_BOARD = "board" +ATTR_CONFIGURED = "configured" ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" +ATTR_ENERGY = "energy" ATTR_HEALTHY = "healthy" ATTR_INSTALLATION_TYPE = "installation_type" ATTR_INTEGRATION_COUNT = "integration_count" diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index 49edf1bcf8c..2dae8d4e629 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -2,8 +2,17 @@ "domain": "analytics", "name": "Analytics", "documentation": "https://www.home-assistant.io/integrations/analytics", - "codeowners": ["@home-assistant/core", "@ludeeus"], - "dependencies": ["api", "websocket_api"], + "codeowners": [ + "@home-assistant/core", + "@ludeeus" + ], + "dependencies": [ + "api", + "websocket_api" + ], + "after_dependencies": [ + "energy" + ], "quality_scale": "internal", "iot_class": "cloud_push" -} +} \ No newline at end of file diff --git a/homeassistant/components/energy/__init__.py b/homeassistant/components/energy/__init__.py index 1e060c1f35b..c856ffb1541 100644 --- a/homeassistant/components/energy/__init__.py +++ b/homeassistant/components/energy/__init__.py @@ -8,6 +8,13 @@ from homeassistant.helpers.typing import ConfigType from . import websocket_api from .const import DOMAIN +from .data import async_get_manager + + +async def is_configured(hass: HomeAssistant) -> bool: + """Return a boolean to indicate if energy is configured.""" + manager = await async_get_manager(hass) + return bool(manager.data != manager.default_preferences()) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ee67a7e3935..a781cb4c662 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -424,3 +424,72 @@ async def test_nightly_endpoint(hass, aioclient_mock): payload = aioclient_mock.mock_calls[0] assert str(payload[1]) == ANALYTICS_ENDPOINT_URL + + +async def test_send_with_no_energy(hass, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ), patch( + "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() + ) as energy_is_configured: + energy_is_configured.return_value = False + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + postdata = aioclient_mock.mock_calls[-1][2] + + assert "energy" not in postdata + + +async def test_send_with_no_energy_config(hass, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert await async_setup_component( + hass, "energy", {"recorder": {"db_url": "sqlite://"}} + ) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ), patch( + "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() + ) as energy_is_configured: + energy_is_configured.return_value = False + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + postdata = aioclient_mock.mock_calls[-1][2] + + assert not postdata["energy"]["configured"] + + +async def test_send_with_energy_config(hass, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert await async_setup_component( + hass, "energy", {"recorder": {"db_url": "sqlite://"}} + ) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ), patch( + "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() + ) as energy_is_configured: + energy_is_configured.return_value = True + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + postdata = aioclient_mock.mock_calls[-1][2] + + assert postdata["energy"]["configured"] diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 80ede3a7548..92e6cf3a5b5 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,7 +1,7 @@ """Test the Energy websocket API.""" import pytest -from homeassistant.components.energy import data +from homeassistant.components.energy import data, is_configured from homeassistant.setup import async_setup_component from tests.common import flush_store @@ -34,6 +34,8 @@ async def test_get_preferences_default(hass, hass_ws_client, hass_storage) -> No manager.data = data.EnergyManager.default_preferences() client = await hass_ws_client(hass) + assert not await is_configured(hass) + await client.send_json({"id": 5, "type": "energy/get_prefs"}) msg = await client.receive_json() @@ -119,6 +121,8 @@ async def test_save_preferences(hass, hass_ws_client, hass_storage) -> None: assert hass_storage[data.STORAGE_KEY]["data"] == new_prefs + assert await is_configured(hass) + # Verify info reflects data. await client.send_json({"id": 7, "type": "energy/info"}) From bebd495e740f8b38bd04ea4ebc085b650d6c5117 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 2 Aug 2021 14:55:52 +0200 Subject: [PATCH 791/818] Allow combinations write_coil/read_coils and write_coils/read_coil for modbus switch (#53856) --- homeassistant/components/modbus/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8936ffc32ac..16be39230db 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -198,6 +198,8 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_COIL, + CALL_TYPE_X_COILS, + CALL_TYPE_X_REGISTER_HOLDINGS, ] ), vol.Optional(CONF_STATE_OFF): cv.positive_int, From e35b5dd7c1a07ced9303a0b8ef7e9116aeeedc5e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 15:00:43 +0200 Subject: [PATCH 792/818] Add RPi.GPIO dependency to rpi_rf integration (#53858) --- homeassistant/components/rpi_rf/manifest.json | 2 +- requirements_all.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index e8806710724..022c84eb13f 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -2,7 +2,7 @@ "domain": "rpi_rf", "name": "Raspberry Pi RF", "documentation": "https://www.home-assistant.io/integrations/rpi_rf", - "requirements": ["rpi-rf==0.9.7"], + "requirements": ["rpi-rf==0.9.7", "RPi.GPIO==0.7.1a4"], "codeowners": [], "iot_class": "assumed_state" } diff --git a/requirements_all.txt b/requirements_all.txt index 7453a3cf2c1..204ef9c75a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -69,6 +69,7 @@ PyXiaomiGateway==0.13.4 # homeassistant.components.bmp280 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio +# homeassistant.components.rpi_rf # RPi.GPIO==0.7.1a4 # homeassistant.components.remember_the_milk From 249fb51d2fc7bce4207ec2299b3de6cb1e43d553 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 16:33:13 +0200 Subject: [PATCH 793/818] Fix cloud accountlinking replacing token data (#53865) --- homeassistant/components/cloud/account_link.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index df93ca6a6ab..5ad7ddcffed 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -143,6 +143,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement async def _async_refresh_token(self, token: dict) -> dict: """Refresh a token.""" - return await account_link.async_fetch_access_token( + new_token = await account_link.async_fetch_access_token( self.hass.data[DOMAIN], self.service, token["refresh_token"] ) + return {**token, **new_token} From df20d69fd2aeaadd779a0f5dd20837eddc35792a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 16:33:27 +0200 Subject: [PATCH 794/818] Add measurement state class to ZHA power devices (#53866) --- homeassistant/components/zha/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b9a86b79063..3c3aba919ed 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -214,6 +214,7 @@ class ElectricalMeasurement(Sensor): SENSOR_ATTR = "active_power" _device_class = DEVICE_CLASS_POWER + _state_class = STATE_CLASS_MEASUREMENT _unit = POWER_WATT @property From b0f6e8c40a36610db18a50975450dfe9d3ab716b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 18:48:17 +0200 Subject: [PATCH 795/818] Fix growat server config entry missing URL key (#53867) --- homeassistant/components/growatt_server/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 9d0aa098051..fe6bdeb70e8 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -598,7 +598,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = config_entry.data username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - url = config[CONF_URL] + url = config.get(CONF_URL, DEFAULT_URL) name = config[CONF_NAME] api = growattServer.GrowattApi() From 66711219c704286b9778a540fedfe734fdcd4683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 2 Aug 2021 19:35:11 +0200 Subject: [PATCH 796/818] Fix issue when data is None (#53875) --- homeassistant/components/energy/__init__.py | 2 ++ tests/components/energy/test_websocket_api.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/energy/__init__.py b/homeassistant/components/energy/__init__.py index c856ffb1541..30a1bf8e877 100644 --- a/homeassistant/components/energy/__init__.py +++ b/homeassistant/components/energy/__init__.py @@ -14,6 +14,8 @@ from .data import async_get_manager async def is_configured(hass: HomeAssistant) -> bool: """Return a boolean to indicate if energy is configured.""" manager = await async_get_manager(hass) + if manager.data is None: + return False return bool(manager.data != manager.default_preferences()) diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 92e6cf3a5b5..a14a8d0986e 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -30,6 +30,7 @@ async def test_get_preferences_no_data(hass, hass_ws_client) -> None: async def test_get_preferences_default(hass, hass_ws_client, hass_storage) -> None: """Test we get preferences.""" + assert not await is_configured(hass) manager = await data.async_get_manager(hass) manager.data = data.EnergyManager.default_preferences() client = await hass_ws_client(hass) From a97e480d82ca5f7f7a37fb038d8f2f5edbd12dd8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 10:42:04 -0700 Subject: [PATCH 797/818] Bump frontend to 20210802.0 (#53876) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 84de9b92c97..f1dfa75864d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210801.0" + "home-assistant-frontend==20210802.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index db6ffd1d071..983831baaa0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210801.0 +home-assistant-frontend==20210802.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 204ef9c75a5..09fb781cf65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210801.0 +home-assistant-frontend==20210802.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04c093ad9a3..347fb0681e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210801.0 +home-assistant-frontend==20210802.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 92cc51370d650531204175ddb8a1c4e004c50bbf Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 2 Aug 2021 18:47:11 +0100 Subject: [PATCH 798/818] Fix watts unit for homekit_controller power sensors (#53877) --- homeassistant/components/homekit_controller/sensor.py | 7 ++++--- .../specific_devices/test_koogeek_p1eu.py | 2 ++ .../specific_devices/test_koogeek_sw2.py | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 6bf8a7fc084..91b62b0d572 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, ) from homeassistant.core import callback @@ -29,19 +30,19 @@ SIMPLE_SENSOR = { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, "state_class": STATE_CLASS_MEASUREMENT, - "unit": "watts", + "unit": POWER_WATT, }, CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, "state_class": STATE_CLASS_MEASUREMENT, - "unit": "watts", + "unit": POWER_WATT, }, CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, "state_class": STATE_CLASS_MEASUREMENT, - "unit": "watts", + "unit": POWER_WATT, }, CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): { "name": "Current Temperature", diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index 6302013223d..1761edb3c8c 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -1,5 +1,6 @@ """Make sure that existing Koogeek P1EU support isn't broken.""" +from homeassistant.const import POWER_WATT from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.homekit_controller.common import ( @@ -48,6 +49,7 @@ async def test_koogeek_p1eu_setup(hass): ) state = await helper.poll_and_get_state() assert state.attributes["friendly_name"] == "Koogeek-P1-A00AA0 - Real Time Energy" + assert state.attributes["unit_of_measurement"] == POWER_WATT # The sensor and switch should be part of the same device assert entry.device_id == device.id diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 1d46b633153..768959e0331 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -6,6 +6,7 @@ This Koogeek device has a custom power sensor that extra handling. It should have 2 entities - the actual switch and a sensor for power usage. """ +from homeassistant.const import POWER_WATT from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.homekit_controller.common import ( @@ -58,6 +59,7 @@ async def test_koogeek_ls1_setup(hass): # Assert that the friendly name is detected correctly assert state.attributes["friendly_name"] == "Koogeek-SW2-187A91 - Real Time Energy" + assert state.attributes["unit_of_measurement"] == POWER_WATT device_registry = dr.async_get(hass) From 2e1f42937d9af4f82b17128ac74778c3e8ba914d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 10:53:52 -0700 Subject: [PATCH 799/818] Bumped version to 2021.8.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0afc18f43bb..bc465d3d742 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From f7e448c8b23cfe4b5cd3ea1a9b101604f61d9ad5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 2 Aug 2021 22:07:37 +0200 Subject: [PATCH 800/818] ESPHome implement light color modes (#53854) --- homeassistant/components/esphome/light.py | 230 ++++++++++++++---- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 190 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index ba968900ef0..b89a75ab76a 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -1,35 +1,45 @@ """Support for ESPHome lights.""" from __future__ import annotations -from typing import Any +from typing import Any, cast -from aioesphomeapi import LightInfo, LightState +from aioesphomeapi import APIVersion, LightColorMode, LightInfo, LightState from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, + ATTR_WHITE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_WHITE, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.util.color as color_util -from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from . import ( + EsphomeEntity, + EsphomeEnumMapper, + esphome_state_property, + platform_async_setup_entry, +) FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} @@ -49,6 +59,22 @@ async def async_setup_entry( ) +_COLOR_MODES: EsphomeEnumMapper[LightColorMode, str] = EsphomeEnumMapper( + { + LightColorMode.UNKNOWN: COLOR_MODE_UNKNOWN, + LightColorMode.ON_OFF: COLOR_MODE_ONOFF, + LightColorMode.BRIGHTNESS: COLOR_MODE_BRIGHTNESS, + LightColorMode.WHITE: COLOR_MODE_WHITE, + LightColorMode.COLOR_TEMPERATURE: COLOR_MODE_COLOR_TEMP, + LightColorMode.COLD_WARM_WHITE: COLOR_MODE_COLOR_TEMP, + LightColorMode.RGB: COLOR_MODE_RGB, + LightColorMode.RGB_WHITE: COLOR_MODE_RGBW, + LightColorMode.RGB_COLOR_TEMPERATURE: COLOR_MODE_RGBWW, + LightColorMode.RGB_COLD_WARM_WHITE: COLOR_MODE_RGBWW, + } +) + + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property # pylint: disable=invalid-overridden-method @@ -56,6 +82,11 @@ async def async_setup_entry( class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """A light implementation for ESPHome.""" + @property + def _supports_color_mode(self) -> bool: + """Return whether the client supports the new color mode system natively.""" + return self._api_version >= APIVersion(1, 6) + @esphome_state_property def is_on(self) -> bool | None: # type: ignore[override] """Return true if the light is on.""" @@ -64,22 +95,80 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" data: dict[str, Any] = {"key": self._static_info.key, "state": True} - if ATTR_HS_COLOR in kwargs: - hue, sat = kwargs[ATTR_HS_COLOR] - red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) - data["rgb"] = (red / 255, green / 255, blue / 255) - if ATTR_FLASH in kwargs: - data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] - if ATTR_TRANSITION in kwargs: - data["transition_length"] = kwargs[ATTR_TRANSITION] - if ATTR_BRIGHTNESS in kwargs: - data["brightness"] = kwargs[ATTR_BRIGHTNESS] / 255 - if ATTR_COLOR_TEMP in kwargs: - data["color_temperature"] = kwargs[ATTR_COLOR_TEMP] - if ATTR_EFFECT in kwargs: - data["effect"] = kwargs[ATTR_EFFECT] - if ATTR_WHITE_VALUE in kwargs: - data["white"] = kwargs[ATTR_WHITE_VALUE] / 255 + # rgb/brightness input is in range 0-255, but esphome uses 0-1 + + if (brightness_ha := kwargs.get(ATTR_BRIGHTNESS)) is not None: + data["brightness"] = brightness_ha / 255 + + if (rgb_ha := kwargs.get(ATTR_RGB_COLOR)) is not None: + rgb = tuple(x / 255 for x in rgb_ha) + color_bri = max(rgb) + # normalize rgb + data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + if self._supports_color_mode: + data["color_brightness"] = color_bri + data["color_mode"] = LightColorMode.RGB + + if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: + # pylint: disable=invalid-name + *rgb, w = tuple(x / 255 for x in rgbw_ha) # type: ignore[assignment] + color_bri = max(rgb) + # normalize rgb + data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + data["white"] = w + if self._supports_color_mode: + data["color_brightness"] = color_bri + data["color_mode"] = LightColorMode.RGB_WHITE + + if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: + # pylint: disable=invalid-name + *rgb, cw, ww = tuple(x / 255 for x in rgbww_ha) # type: ignore[assignment] + color_bri = max(rgb) + # normalize rgb + data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + modes = self._native_supported_color_modes + if ( + self._supports_color_mode + and LightColorMode.RGB_COLD_WARM_WHITE in modes + ): + data["cold_white"] = cw + data["warm_white"] = ww + target_mode = LightColorMode.RGB_COLD_WARM_WHITE + else: + # need to convert cw+ww part to white+color_temp + white = data["white"] = max(cw, ww) + if white != 0: + min_ct = self.min_mireds + max_ct = self.max_mireds + ct_ratio = ww / (cw + ww) + data["color_temperature"] = min_ct + ct_ratio * (max_ct - min_ct) + target_mode = LightColorMode.RGB_COLOR_TEMPERATURE + + if self._supports_color_mode: + data["color_brightness"] = color_bri + data["color_mode"] = target_mode + + if (flash := kwargs.get(ATTR_FLASH)) is not None: + data["flash_length"] = FLASH_LENGTHS[flash] + + if (transition := kwargs.get(ATTR_TRANSITION)) is not None: + data["transition_length"] = transition + + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: + data["color_temperature"] = color_temp + if self._supports_color_mode: + data["color_mode"] = LightColorMode.COLOR_TEMPERATURE + + if (effect := kwargs.get(ATTR_EFFECT)) is not None: + data["effect"] = effect + + if (white_ha := kwargs.get(ATTR_WHITE)) is not None: + # ESPHome multiplies brightness and white together for final brightness + # HA only sends `white` in turn_on, and reads total brightness through brightness property + data["brightness"] = white_ha / 255 + data["white"] = 1.0 + data["color_mode"] = LightColorMode.WHITE + await self._client.light_command(**data) async def async_turn_off(self, **kwargs: Any) -> None: @@ -97,10 +186,65 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return round(self._state.brightness * 255) @esphome_state_property - def hs_color(self) -> tuple[float, float] | None: - """Return the hue and saturation color value [float, float].""" - return color_util.color_RGB_to_hs( - self._state.red * 255, self._state.green * 255, self._state.blue * 255 + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if not self._supports_color_mode: + supported = self.supported_color_modes + if not supported: + return None + return next(iter(supported)) + + return _COLOR_MODES.from_esphome(self._state.color_mode) + + @esphome_state_property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + if not self._supports_color_mode: + return ( + round(self._state.red * 255), + round(self._state.green * 255), + round(self._state.blue * 255), + ) + + return ( + round(self._state.red * self._state.color_brightness * 255), + round(self._state.green * self._state.color_brightness * 255), + round(self._state.blue * self._state.color_brightness * 255), + ) + + @esphome_state_property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + white = round(self._state.white * 255) + rgb = cast("tuple[int, int, int]", self.rgb_color) + return (*rgb, white) + + @esphome_state_property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value [int, int, int, int, int].""" + rgb = cast("tuple[int, int, int]", self.rgb_color) + if ( + not self._supports_color_mode + or self._state.color_mode != LightColorMode.RGB_COLD_WARM_WHITE + ): + # Try to reverse white + color temp to cwww + min_ct = self._static_info.min_mireds + max_ct = self._static_info.max_mireds + color_temp = self._state.color_temperature + white = self._state.white + + ww_frac = (color_temp - min_ct) / (max_ct - min_ct) + cw_frac = 1 - ww_frac + + return ( + *rgb, + round(white * cw_frac / max(cw_frac, ww_frac) * 255), + round(white * ww_frac / max(cw_frac, ww_frac) * 255), + ) + return ( + *rgb, + round(self._state.cold_white * 255), + round(self._state.warm_white * 255), ) @esphome_state_property @@ -108,33 +252,33 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """Return the CT color value in mireds.""" return self._state.color_temperature - @esphome_state_property - def white_value(self) -> int | None: - """Return the white value of this light between 0..255.""" - return round(self._state.white * 255) - @esphome_state_property def effect(self) -> str | None: """Return the current effect.""" return self._state.effect + @property + def _native_supported_color_modes(self) -> list[LightColorMode]: + return self._static_info.supported_color_modes_compat(self._api_version) + @property def supported_features(self) -> int: """Flag supported features.""" flags = SUPPORT_FLASH - if self._static_info.supports_brightness: - flags |= SUPPORT_BRIGHTNESS + + # All color modes except UNKNOWN,ON_OFF support transition + modes = self._native_supported_color_modes + if any(m not in (LightColorMode.UNKNOWN, LightColorMode.ON_OFF) for m in modes): flags |= SUPPORT_TRANSITION - if self._static_info.supports_rgb: - flags |= SUPPORT_COLOR - if self._static_info.supports_white_value: - flags |= SUPPORT_WHITE_VALUE - if self._static_info.supports_color_temperature: - flags |= SUPPORT_COLOR_TEMP if self._static_info.effects: flags |= SUPPORT_EFFECT return flags + @property + def supported_color_modes(self) -> set[str] | None: + """Flag supported color modes.""" + return set(map(_COLOR_MODES.from_esphome, self._native_supported_color_modes)) + @property def effect_list(self) -> list[str]: """Return the list of supported effects.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 218c349e5a8..a6792808720 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==5.1.1"], + "requirements": ["aioesphomeapi==6.0.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 09fb781cf65..3622b435c8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.1.1 +aioesphomeapi==6.0.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 347fb0681e1..79abf95dd58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.1.1 +aioesphomeapi==6.0.0 # homeassistant.components.flo aioflo==0.4.1 From be6cb2e79240c3461f765c29becf518c70f16ba9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 14:44:15 -0700 Subject: [PATCH 801/818] Bump aiohue to 2.6.1 (#53887) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 05e69948218..32b3cd4ee51 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.6.0"], + "requirements": ["aiohue==2.6.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3622b435c8e..84e076fabec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.0 +aiohue==2.6.1 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79abf95dd58..75f6c0a1239 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -122,7 +122,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.0 +aiohue==2.6.1 # homeassistant.components.apache_kafka aiokafka==0.6.0 From d74ca2529126febc49a15dc4c036874d776c8a1c Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 2 Aug 2021 20:50:57 -0700 Subject: [PATCH 802/818] Handle powerConsumption reports with null value (#53888) --- .../components/smartthings/sensor.py | 11 +++++++++- tests/components/smartthings/test_sensor.py | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 5059bcc4403..cb8fa4bb6d2 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -554,6 +554,8 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): """Init the class.""" super().__init__(device) self.report_name = report_name + # This is an exception for STATE_CLASS_MEASUREMENT per @balloob + self._attr_state_class = STATE_CLASS_MEASUREMENT @property def name(self) -> str: @@ -569,7 +571,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): def state(self): """Return the state of the sensor.""" value = self._device.status.attributes[Attribute.power_consumption].value - if value.get(self.report_name) is None: + if value is None or value.get(self.report_name) is None: return None if self.report_name == "power": return value[self.report_name] @@ -588,3 +590,10 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): if self.report_name == "power": return POWER_WATT return ENERGY_KILO_WATT_HOUR + + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + if self.report_name != "power": + return utc_from_timestamp(0) + return None diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index fa849a3cc67..70103c3a837 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -187,6 +187,28 @@ async def test_power_consumption_sensor(hass, device_factory): assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" + device = device_factory( + "vacuum", + [Capability.power_consumption_report], + {Attribute.power_consumption: {}}, + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + # Act + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + # Assert + state = hass.states.get("sensor.vacuum_energy") + assert state + assert state.state == "unknown" + entry = entity_registry.async_get("sensor.vacuum_energy") + assert entry + assert entry.unique_id == f"{device.device_id}.energy" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + async def test_update_from_signal(hass, device_factory): """Test the binary_sensor updates when receiving a signal.""" From 8b7bdc9b67cb54c92b96d889969aec734f211671 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 2 Aug 2021 21:52:44 -0600 Subject: [PATCH 803/818] Only show a SimpliSafe code entry when one exists (#53894) --- .../components/simplisafe/alarm_control_panel.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 5c50d6a343e..7e0b64a8c32 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,8 +1,6 @@ """Support for SimpliSafe alarm control panels.""" from __future__ import annotations -import re - from simplipy.errors import SimplipyError from simplipy.system import SystemStates from simplipy.system.v2 import SystemV2 @@ -72,12 +70,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") - if isinstance( - self._simplisafe.config_entry.options.get(CONF_CODE), str - ) and re.search("^\\d+$", self._simplisafe.config_entry.options[CONF_CODE]): - self._attr_code_format = FORMAT_NUMBER - else: - self._attr_code_format = FORMAT_TEXT + if CONF_CODE in self._simplisafe.config_entry.options: + if self._simplisafe.config_entry.options[CONF_CODE].isdigit(): + self._attr_code_format = FORMAT_NUMBER + else: + self._attr_code_format = FORMAT_TEXT self._attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY self._last_event = None From 120122ffe2bfec5ceaff6ba0c3e8359417b5732b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 21:39:53 -0700 Subject: [PATCH 804/818] Bump frontend to 20210803.0 (#53897) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f1dfa75864d..a94bcf44327 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210802.0" + "home-assistant-frontend==20210803.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 983831baaa0..9dfcc96d6da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210802.0 +home-assistant-frontend==20210803.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 84e076fabec..edf77033645 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210802.0 +home-assistant-frontend==20210803.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75f6c0a1239..754097e8317 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210802.0 +home-assistant-frontend==20210803.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5536da18a6a57ddba9a3d9bdc7111da8d97eaf74 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 21:41:32 -0700 Subject: [PATCH 805/818] Bumped version to 2021.8.0b9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bc465d3d742..d5b48312f3c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 30497eff0ed0d0070c718ce4988906f1edcbe8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 3 Aug 2021 11:58:27 +0200 Subject: [PATCH 806/818] Add user to homeassistant system health (#53902) --- .../components/homeassistant/strings.json | 3 ++- .../components/homeassistant/system_health.py | 1 + homeassistant/helpers/system_info.py | 5 ++++- tests/helpers/test_system_info.py | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 7da4a5a9d8a..09be9283b5c 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -4,6 +4,7 @@ "arch": "CPU Architecture", "dev": "Development", "docker": "Docker", + "user": "User", "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", @@ -14,4 +15,4 @@ "virtualenv": "Virtual Environment" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index ff3562a24f9..f13278ddfeb 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -22,6 +22,7 @@ async def system_health_info(hass): "dev": info.get("dev"), "hassio": info.get("hassio"), "docker": info.get("docker"), + "user": info.get("user"), "virtualenv": info.get("virtualenv"), "python_version": info.get("python_version"), "os_name": info.get("os_name"), diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 6d6c912f8c9..766fa90af96 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -1,6 +1,7 @@ """Helper to gather system info.""" from __future__ import annotations +from getpass import getuser import os import platform from typing import Any @@ -22,6 +23,7 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: "virtualenv": is_virtual_env(), "python_version": platform.python_version(), "docker": False, + "user": getuser(), "arch": platform.machine(), "timezone": str(hass.config.time_zone), "os_name": platform.system(), @@ -37,7 +39,8 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: # Determine installation type on current data if info_object["docker"]: - info_object["installation_type"] = "Home Assistant Container" + if info_object["user"] == "root": + info_object["installation_type"] = "Home Assistant Container" elif is_virtual_env(): info_object["installation_type"] = "Home Assistant Core" diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index e27114c1a13..fd9d488596f 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -1,5 +1,6 @@ """Tests for the system info helper.""" import json +from unittest.mock import patch from homeassistant.const import __version__ as current_version @@ -9,4 +10,20 @@ async def test_get_system_info(hass): info = await hass.helpers.system_info.async_get_system_info() assert isinstance(info, dict) assert info["version"] == current_version + assert info["user"] is not None assert json.dumps(info) is not None + + +async def test_container_installationtype(hass): + """Test container installation type.""" + with patch("platform.system", return_value="Linux"), patch( + "os.path.isfile", return_value=True + ): + info = await hass.helpers.system_info.async_get_system_info() + assert info["installation_type"] == "Home Assistant Container" + + with patch("platform.system", return_value="Linux"), patch( + "os.path.isfile", return_value=True + ), patch("homeassistant.helpers.system_info.getuser", return_value="user"): + info = await hass.helpers.system_info.async_get_system_info() + assert info["installation_type"] == "Unknown" From 922c0dc8bee7ce2be94052c7497949eecc580e6f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Aug 2021 23:39:35 +1200 Subject: [PATCH 807/818] Bump aioesphomeapi to 6.0.1 (#53905) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a6792808720..22fa33091fd 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==6.0.0"], + "requirements": ["aioesphomeapi==6.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index edf77033645..402ca09358c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==6.0.0 +aioesphomeapi==6.0.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 754097e8317..96d8f9660d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==6.0.0 +aioesphomeapi==6.0.1 # homeassistant.components.flo aioflo==0.4.1 From 07604e60e5ba6967bdc5e875bdc98897f30e8ddb Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Tue, 3 Aug 2021 12:52:59 +0100 Subject: [PATCH 808/818] Bump pyroon to 0.0.38 (#53906) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 354117e8fe4..f4864571735 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.37"], + "requirements": ["roonapi==0.0.38"], "codeowners": ["@pavoni"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 402ca09358c..46623a817a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.37 +roonapi==0.0.38 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96d8f9660d0..8099effd980 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1125,7 +1125,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.37 +roonapi==0.0.38 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From e4fd43ed7ca8c3a114844e5ddcdc47103d6d2b2b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 3 Aug 2021 15:58:30 +0200 Subject: [PATCH 809/818] Use `SelectEntityDescription` for Xiaomi Miio integration (#53907) * Use SelectEntityDescription * Use SelectEntityDescription * Remove service field from XiaomiMiioSelectDescription class * Fix typo * Use lowercase for options --- .../components/xiaomi_miio/select.py | 67 +++++++++---------- .../xiaomi_miio/strings.select.json | 9 +++ 2 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/strings.select.json diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 77aba961244..c5cee6221fa 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -1,11 +1,13 @@ """Support led_brightness for Mi Air Humidifier.""" +from __future__ import annotations + from dataclasses import dataclass from enum import Enum from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness -from homeassistant.components.select import SelectEntity +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.core import callback from .const import ( @@ -20,7 +22,6 @@ from .const import ( MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, MODELS_HUMIDIFIER, - SERVICE_SET_LED_BRIGHTNESS, ) from .device import XiaomiCoordinatedMiioEntity @@ -36,23 +37,19 @@ LED_BRIGHTNESS_REVERSE_MAP_MIOT = { @dataclass -class SelectorType: - """Class that holds device specific info for a xiaomi aqara or humidifier selectors.""" +class XiaomiMiioSelectDescription(SelectEntityDescription): + """A class that describes select entities.""" - name: str = None - icon: str = None - short_name: str = None - options: list = None - service: str = None + options: tuple = () SELECTOR_TYPES = { - FEATURE_SET_LED_BRIGHTNESS: SelectorType( - name="Led brightness", + FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioSelectDescription( + key=ATTR_LED_BRIGHTNESS, + name="Led Brightness", icon="mdi:brightness-6", - short_name=ATTR_LED_BRIGHTNESS, - options=["Bright", "Dim", "Off"], - service=SERVICE_SET_LED_BRIGHTNESS, + device_class="xiaomi_miio__led_brightness", + options=("bright", "dim", "off"), ), } @@ -65,7 +62,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: entity_class = XiaomiAirHumidifierSelector @@ -76,17 +72,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): else: return - for selector in SELECTOR_TYPES.values(): - entities.append( - entity_class( - f"{config_entry.title} {selector.name}", - device, - config_entry, - f"{selector.short_name}_{config_entry.unique_id}", - selector, - coordinator, - ) + description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] + entities.append( + entity_class( + f"{config_entry.title} {description.name}", + device, + config_entry, + f"{description.key}_{config_entry.unique_id}", + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, ) + ) async_add_entities(entities) @@ -94,12 +90,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, selector, coordinator): + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" super().__init__(name, device, entry, unique_id, coordinator) - self._attr_icon = selector.icon - self._controller = selector - self._attr_options = self._controller.options + self._attr_options = list(description.options) + self.entity_description = description @staticmethod def _extract_value_from_attribute(state, attribute): @@ -113,33 +108,33 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" - def __init__(self, name, device, entry, unique_id, controller, coordinator): + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, controller, coordinator) + super().__init__(name, device, entry, unique_id, coordinator, description) self._current_led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self._controller.short_name + self.coordinator.data, self.entity_description.key ) @callback def _handle_coordinator_update(self): """Fetch state from the device.""" self._current_led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self._controller.short_name + self.coordinator.data, self.entity_description.key ) self.async_write_ha_state() @property def current_option(self): """Return the current option.""" - return self.led_brightness + return self.led_brightness.lower() async def async_select_option(self, option: str) -> None: """Set an option of the miio device.""" if option not in self.options: raise ValueError( - f"Selection '{option}' is not a valid {self._controller.name}" + f"Selection '{option}' is not a valid {self.entity_description.name}" ) - await self.async_set_led_brightness(option) + await self.async_set_led_brightness(option.title()) @property def led_brightness(self): diff --git a/homeassistant/components/xiaomi_miio/strings.select.json b/homeassistant/components/xiaomi_miio/strings.select.json new file mode 100644 index 00000000000..80edde042ce --- /dev/null +++ b/homeassistant/components/xiaomi_miio/strings.select.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Bright", + "dim": "Dim", + "off": "Off" + } + } + } \ No newline at end of file From 4e2c1747413f03e2302dce878714856ffb3bbd31 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 3 Aug 2021 08:56:15 -0600 Subject: [PATCH 810/818] Handle scenario where SimpliSafe code is falsey (#53912) --- homeassistant/components/simplisafe/alarm_control_panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 7e0b64a8c32..8520cd2b50f 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -70,8 +70,8 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") - if CONF_CODE in self._simplisafe.config_entry.options: - if self._simplisafe.config_entry.options[CONF_CODE].isdigit(): + if code := self._simplisafe.config_entry.options.get(CONF_CODE): + if code.isdigit(): self._attr_code_format = FORMAT_NUMBER else: self._attr_code_format = FORMAT_TEXT From 54ac889362a92a4442054db41a22d5d13f14fe5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Aug 2021 12:09:10 -0500 Subject: [PATCH 811/818] Enforce maximum length for HomeKit characteristics (#53913) --- .../components/homekit/accessories.py | 17 ++++++--- homeassistant/components/homekit/const.py | 7 ++++ homeassistant/components/homekit/type_fans.py | 4 +- .../components/homekit/type_media_players.py | 3 +- .../components/homekit/type_remotes.py | 7 +++- tests/components/homekit/test_accessories.py | 37 +++++++++++++------ 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 03d00c42a91..49ba1103ac5 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -64,6 +64,11 @@ from .const import ( HK_NOT_CHARGABLE, HK_NOT_CHARGING, MANUFACTURER, + MAX_MANUFACTURER_LENGTH, + MAX_MODEL_LENGTH, + MAX_NAME_LENGTH, + MAX_SERIAL_LENGTH, + MAX_VERSION_LENGTH, SERV_BATTERY_SERVICE, SERVICE_HOMEKIT_RESET_ACCESSORY, TYPE_FAUCET, @@ -217,7 +222,9 @@ class HomeAccessory(Accessory): **kwargs, ): """Initialize a Accessory object.""" - super().__init__(driver=driver, display_name=name, aid=aid, *args, **kwargs) + super().__init__( + driver=driver, display_name=name[:MAX_NAME_LENGTH], aid=aid, *args, **kwargs + ) self.config = config or {} domain = split_entity_id(entity_id)[0].replace("_", " ") @@ -237,10 +244,10 @@ class HomeAccessory(Accessory): sw_version = __version__ self.set_info_service( - manufacturer=manufacturer, - model=model, - serial_number=entity_id, - firmware_revision=sw_version, + manufacturer=manufacturer[:MAX_MANUFACTURER_LENGTH], + model=model[:MAX_MODEL_LENGTH], + serial_number=entity_id[:MAX_SERIAL_LENGTH], + firmware_revision=sw_version[:MAX_VERSION_LENGTH], ) self.category = category diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 4fecd64b2b2..7f413ef78df 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -296,3 +296,10 @@ CONFIG_OPTIONS = [ CONF_ENTITY_CONFIG, CONF_HOMEKIT_MODE, ] + +# ### Maximum Lengths ### +MAX_NAME_LENGTH = 64 +MAX_SERIAL_LENGTH = 64 +MAX_MODEL_LENGTH = 64 +MAX_VERSION_LENGTH = 64 +MAX_MANUFACTURER_LENGTH = 64 diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 1efb3b6c8be..1a0bb41774c 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -39,6 +39,7 @@ from .const import ( CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, + MAX_NAME_LENGTH, PROP_MIN_STEP, SERV_FANV2, SERV_SWITCH, @@ -100,7 +101,8 @@ class Fan(HomeAccessory): preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_fan.add_linked_service(preset_serv) preset_serv.configure_char( - CHAR_NAME, value=f"{self.display_name} {preset_mode}" + CHAR_NAME, + value=f"{self.display_name} {preset_mode}"[:MAX_NAME_LENGTH], ) self.preset_mode_chars[preset_mode] = preset_serv.configure_char( diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 5cd27109bd8..081053d2591 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -55,6 +55,7 @@ from .const import ( FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, KEY_PLAY_PAUSE, + MAX_NAME_LENGTH, SERV_SWITCH, SERV_TELEVISION_SPEAKER, ) @@ -134,7 +135,7 @@ class MediaPlayer(HomeAccessory): def generate_service_name(self, mode): """Generate name for individual service.""" - return f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}" + return f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}"[:MAX_NAME_LENGTH] def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 718671dfd1d..9e54221430c 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -47,6 +47,7 @@ from .const import ( KEY_PREVIOUS_TRACK, KEY_REWIND, KEY_SELECT, + MAX_NAME_LENGTH, SERV_INPUT_SOURCE, SERV_TELEVISION, ) @@ -120,8 +121,10 @@ class RemoteInputSelectAccessory(HomeAccessory): SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] ) serv_tv.add_linked_service(serv_input) - serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) - serv_input.configure_char(CHAR_NAME, value=source) + serv_input.configure_char( + CHAR_CONFIGURED_NAME, value=source[:MAX_NAME_LENGTH] + ) + serv_input.configure_char(CHAR_NAME, value=source[:MAX_NAME_LENGTH]) serv_input.configure_char(CHAR_IDENTIFIER, value=index) serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) input_type = 3 if "hdmi" in source.lower() else 0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 84ed61322a2..257be293fc0 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -66,7 +66,7 @@ async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver): async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = "sensor.accessory" - entity_id2 = "light.accessory" + entity_id2 = "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum_maximum_maximum_maximum_allowed_length" hass.states.async_set(entity_id, None) hass.states.async_set(entity_id2, STATE_UNAVAILABLE) @@ -94,27 +94,42 @@ async def test_home_accessory(hass, hk_driver): assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" assert serv.get_characteristic(CHAR_MANUFACTURER).value == f"{MANUFACTURER} Light" assert serv.get_characteristic(CHAR_MODEL).value == "Light" - assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory" + assert ( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value + == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" + ) acc3 = HomeAccessory( hass, hk_driver, - "Home Accessory", + "Home Accessory that exceeds the maximum maximum maximum maximum maximum maximum length", entity_id2, 3, { - ATTR_MODEL: "Awesome", - ATTR_MANUFACTURER: "Lux Brands", - ATTR_SOFTWARE_VERSION: "0.4.3", - ATTR_INTEGRATION: "luxe", + ATTR_MODEL: "Awesome Model that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_MANUFACTURER: "Lux Brands that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_SOFTWARE_VERSION: "0.4.3 that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_INTEGRATION: "luxe that exceeds the maximum maximum maximum maximum maximum maximum length", }, ) assert acc3.available is False serv = acc3.services[0] # SERV_ACCESSORY_INFO - assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" - assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Lux Brands" - assert serv.get_characteristic(CHAR_MODEL).value == "Awesome" - assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory" + assert ( + serv.get_characteristic(CHAR_NAME).value + == "Home Accessory that exceeds the maximum maximum maximum maximum " + ) + assert ( + serv.get_characteristic(CHAR_MANUFACTURER).value + == "Lux Brands that exceeds the maximum maximum maximum maximum maxi" + ) + assert ( + serv.get_characteristic(CHAR_MODEL).value + == "Awesome Model that exceeds the maximum maximum maximum maximum m" + ) + assert ( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value + == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" + ) hass.states.async_set(entity_id, "on") await hass.async_block_till_done() From 7a8676dc831577290b2ffd1f183dba2cf8eccfb0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Aug 2021 11:16:00 -0700 Subject: [PATCH 812/818] Handle Shelly get name on uninitialized device (#53917) --- homeassistant/components/shelly/logbook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index 5b0ada6f166..deac3b5c05b 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -29,7 +29,7 @@ def async_describe_events( def async_describe_shelly_click_event(event: EventType) -> dict[str, str]: """Describe shelly.click logbook event.""" wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID]) - if wrapper: + if wrapper and wrapper.device.initialized: device_name = get_device_name(wrapper.device) else: device_name = event.data[ATTR_DEVICE] From 0342c0da3392b864731cc0d1bd051502364a9a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 3 Aug 2021 20:20:12 +0200 Subject: [PATCH 813/818] Limit API usage for Uptime Robot (#53918) --- .../components/uptimerobot/binary_sensor.py | 137 +++++++++++------- 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 6c0bb63c70f..dd7254fb1ca 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,6 +1,9 @@ """A platform that to monitor Uptime Robot monitors.""" +from dataclasses import dataclass +from datetime import timedelta import logging +import async_timeout from pyuptimerobot import UptimeRobot import voluptuous as vol @@ -8,9 +11,17 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, PLATFORM_SCHEMA, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) _LOGGER = logging.getLogger(__name__) @@ -21,69 +32,91 @@ ATTRIBUTION = "Data provided by Uptime Robot" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -def setup_platform(hass, config, add_entities, discovery_info=None): +@dataclass +class UptimeRobotBinarySensorEntityDescription(BinarySensorEntityDescription): + """Entity description for UptimeRobotBinarySensor.""" + + target: str = "" + + +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None +): """Set up the Uptime Robot binary_sensors.""" + uptime_robot_api = UptimeRobot() + api_key = config[CONF_API_KEY] - up_robot = UptimeRobot() - api_key = config.get(CONF_API_KEY) - monitors = up_robot.getMonitors(api_key) + async def async_update_data(): + """Fetch data from API UptimeRobot API.""" - devices = [] - if not monitors or monitors.get("stat") != "ok": + def api_wrapper(): + return uptime_robot_api.getMonitors(api_key) + + async with async_timeout.timeout(10): + monitors = await hass.async_add_executor_job(api_wrapper) + if not monitors or monitors.get("stat") != "ok": + raise UpdateFailed("Error communicating with Uptime Robot API") + return monitors + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="uptimerobot", + update_method=async_update_data, + update_interval=timedelta(seconds=60), + ) + + await coordinator.async_refresh() + + if not coordinator.data or coordinator.data.get("stat") != "ok": _LOGGER.error("Error connecting to Uptime Robot") - return + raise PlatformNotReady() - for monitor in monitors["monitors"]: - devices.append( + async_add_entities( + [ UptimeRobotBinarySensor( - api_key, - up_robot, - monitor["id"], - monitor["friendly_name"], - monitor["url"], + coordinator, + UptimeRobotBinarySensorEntityDescription( + key=monitor["id"], + name=monitor["friendly_name"], + target=monitor["url"], + device_class=DEVICE_CLASS_CONNECTIVITY, + ), ) - ) - - add_entities(devices, True) + for monitor in coordinator.data["monitors"] + ], + True, + ) -class UptimeRobotBinarySensor(BinarySensorEntity): +class UptimeRobotBinarySensor(BinarySensorEntity, CoordinatorEntity): """Representation of a Uptime Robot binary sensor.""" - def __init__(self, api_key, up_robot, monitor_id, name, target): + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: UptimeRobotBinarySensorEntityDescription, + ) -> None: """Initialize Uptime Robot the binary sensor.""" - self._api_key = api_key - self._monitor_id = str(monitor_id) - self._name = name - self._target = target - self._up_robot = up_robot - self._state = None + super().__init__(coordinator) + self.coordinator = coordinator + self.entity_description = description + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_TARGET: self.entity_description.target, + } - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_CONNECTIVITY - - @property - def extra_state_attributes(self): - """Return the state attributes of the binary sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self._target} - - def update(self): + async def async_update(self): """Get the latest state of the binary sensor.""" - monitor = self._up_robot.getMonitors(self._api_key, self._monitor_id) - if not monitor or monitor.get("stat") != "ok": - _LOGGER.warning("Failed to get new state") - return - status = monitor["monitors"][0]["status"] - self._state = 1 if status == 2 else 0 + if monitor := get_monitor_by_id( + self.coordinator.data.get("monitors", []), self.entity_description.key + ): + self._attr_is_on = monitor["status"] == 2 + + +def get_monitor_by_id(monitors, monitor_id): + """Return the monitor object matching the id.""" + filtered = [monitor for monitor in monitors if monitor["id"] == monitor_id] + if len(filtered) == 0: + return + return filtered[0] From 4fdd354745ace6145d7f03775e96a1801ad28fb6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 3 Aug 2021 22:50:14 +0200 Subject: [PATCH 814/818] Limit zwave_js meter sensor last reset (#53921) --- homeassistant/components/zwave_js/sensor.py | 20 ++++++++------ tests/components/zwave_js/common.py | 3 ++- tests/components/zwave_js/test_sensor.py | 29 ++++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 304a80f7940..7b491661e68 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -248,20 +248,20 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): # Entity class attributes self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_last_reset = dt.utc_from_timestamp(0) + if self.device_class == DEVICE_CLASS_ENERGY: + self._attr_last_reset = dt.utc_from_timestamp(0) @callback def async_update_last_reset( self, node: ZwaveNode, endpoint: int, meter_type: int | None ) -> None: """Update last reset.""" - # If the signal is not for this node or is for a different endpoint, ignore it - if self.info.node != node or self.info.primary_value.endpoint != endpoint: - return - # If a meter type was specified and doesn't match this entity's meter type, - # ignore it + # If the signal is not for this node or is for a different endpoint, + # or a meter type was specified and doesn't match this entity's meter type: if ( - meter_type is not None + self.info.node != node + or self.info.primary_value.endpoint != endpoint + or meter_type is not None and self.info.primary_value.metadata.cc_specific.get("meterType") != meter_type ): @@ -274,6 +274,10 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): """Call when entity is added.""" await super().async_added_to_hass() + # If the meter is not an accumulating meter type, do not reset. + if self.device_class != DEVICE_CLASS_ENERGY: + return + # Restore the last reset time from stored state restored_state = await self.async_get_last_state() if restored_state and ATTR_LAST_RESET in restored_state.attributes: @@ -310,7 +314,7 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): primary_value.endpoint, options, ) - self._attr_last_reset = dt.utcnow() + # Notify meters that may have been reset async_dispatcher_send( self.hass, diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 8c8a3f2e576..44943fed9fb 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -33,7 +33,8 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = ( "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode" ) ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" -METER_SENSOR = "sensor.smart_switch_6_electric_consumed_v" +METER_ENERGY_SENSOR = "sensor.smart_switch_6_electric_consumed_kwh" +METER_VOLTAGE_SENSOR = "sensor.smart_switch_6_electric_consumed_v" DATETIME_ZERO = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc) DATETIME_LAST_RESET = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 9fa4152ad6b..04583559421 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -36,7 +36,8 @@ from .common import ( HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, INDICATOR_SENSOR, - METER_SENSOR, + METER_ENERGY_SENSOR, + METER_VOLTAGE_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, VOLTAGE_SENSOR, @@ -202,9 +203,13 @@ async def test_reset_meter( client.async_send_command.return_value = {} client.async_send_command_no_wait.return_value = {} + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes + # Validate that the sensor last reset is starting from nothing assert ( - hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] == DATETIME_ZERO.isoformat() ) @@ -215,13 +220,13 @@ async def test_reset_meter( DOMAIN, SERVICE_RESET_METER, { - ATTR_ENTITY_ID: METER_SENSOR, + ATTR_ENTITY_ID: METER_ENERGY_SENSOR, }, blocking=True, ) assert ( - hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] == DATETIME_LAST_RESET.isoformat() ) @@ -232,6 +237,10 @@ async def test_reset_meter( assert args["endpoint"] == 0 assert args["args"] == [] + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes + client.async_send_command_no_wait.reset_mock() # Test successful meter reset call with options @@ -239,7 +248,7 @@ async def test_reset_meter( DOMAIN, SERVICE_RESET_METER, { - ATTR_ENTITY_ID: METER_SENSOR, + ATTR_ENTITY_ID: METER_ENERGY_SENSOR, ATTR_METER_TYPE: 1, ATTR_VALUE: 2, }, @@ -253,6 +262,10 @@ async def test_reset_meter( assert args["endpoint"] == 0 assert args["args"] == [{"type": 1, "targetValue": 2}] + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes + client.async_send_command_no_wait.reset_mock() @@ -265,6 +278,10 @@ async def test_restore_last_reset( ): """Test restoring last_reset on setup.""" assert ( - hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] == DATETIME_LAST_RESET.isoformat() ) + + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes From af81dda1e2fa308afc4c6384a23293652d7ce8bb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Aug 2021 02:10:33 +0200 Subject: [PATCH 815/818] Update frontend to 20210803.2 (#53923) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index a94bcf44327..fbf676687cb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210803.0" + "home-assistant-frontend==20210803.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9dfcc96d6da..7b9fe917279 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210803.0 +home-assistant-frontend==20210803.2 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 46623a817a9..f15f5cf66ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210803.0 +home-assistant-frontend==20210803.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8099effd980..29a8352f433 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210803.0 +home-assistant-frontend==20210803.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 38df475936af93c7036179b9a038f1087f6ec9a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Aug 2021 21:04:22 -0700 Subject: [PATCH 816/818] Bumped version to 2021.8.0b10 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d5b48312f3c..706942e7faa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 9ec516e1d4577eae43ab5cf811b808dc54338d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 4 Aug 2021 09:29:51 +0200 Subject: [PATCH 817/818] Address review comments for 53918 (#53927) --- .../components/uptimerobot/binary_sensor.py | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index dd7254fb1ca..e1684d64924 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,5 +1,4 @@ """A platform that to monitor Uptime Robot monitors.""" -from dataclasses import dataclass from datetime import timedelta import logging @@ -32,13 +31,6 @@ ATTRIBUTION = "Data provided by Uptime Robot" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -@dataclass -class UptimeRobotBinarySensorEntityDescription(BinarySensorEntityDescription): - """Entity description for UptimeRobotBinarySensor.""" - - target: str = "" - - async def async_setup_platform( hass: HomeAssistant, config, async_add_entities, discovery_info=None ): @@ -46,12 +38,11 @@ async def async_setup_platform( uptime_robot_api = UptimeRobot() api_key = config[CONF_API_KEY] + def api_wrapper(): + return uptime_robot_api.getMonitors(api_key) + async def async_update_data(): """Fetch data from API UptimeRobot API.""" - - def api_wrapper(): - return uptime_robot_api.getMonitors(api_key) - async with async_timeout.timeout(10): monitors = await hass.async_add_executor_job(api_wrapper) if not monitors or monitors.get("stat") != "ok": @@ -76,16 +67,15 @@ async def async_setup_platform( [ UptimeRobotBinarySensor( coordinator, - UptimeRobotBinarySensorEntityDescription( + BinarySensorEntityDescription( key=monitor["id"], name=monitor["friendly_name"], - target=monitor["url"], device_class=DEVICE_CLASS_CONNECTIVITY, ), + target=monitor["url"], ) for monitor in coordinator.data["monitors"] ], - True, ) @@ -95,28 +85,28 @@ class UptimeRobotBinarySensor(BinarySensorEntity, CoordinatorEntity): def __init__( self, coordinator: DataUpdateCoordinator, - description: UptimeRobotBinarySensorEntityDescription, + description: BinarySensorEntityDescription, + target: str, ) -> None: """Initialize Uptime Robot the binary sensor.""" super().__init__(coordinator) - self.coordinator = coordinator self.entity_description = description + self._target = target self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_TARGET: self.entity_description.target, + ATTR_TARGET: self._target, } - async def async_update(self): - """Get the latest state of the binary sensor.""" - if monitor := get_monitor_by_id( - self.coordinator.data.get("monitors", []), self.entity_description.key + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + if monitor := next( + ( + monitor + for monitor in self.coordinator.data.get("monitors", []) + if monitor["id"] == self.entity_description.key + ), + None, ): - self._attr_is_on = monitor["status"] == 2 - - -def get_monitor_by_id(monitors, monitor_id): - """Return the monitor object matching the id.""" - filtered = [monitor for monitor in monitors if monitor["id"] == monitor_id] - if len(filtered) == 0: - return - return filtered[0] + return monitor["status"] == 2 + return False From 4de3f031cc82e78c0632835b9ce8edcb55877581 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Aug 2021 10:46:20 +0200 Subject: [PATCH 818/818] Bumped version to 2021.8.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 706942e7faa..b95825ae39e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)